MediaWiki  master
Maintenance.php
Go to the documentation of this file.
1 <?php
30 
61 abstract class Maintenance {
66  public const DB_NONE = 0;
67  public const DB_STD = 1;
68  public const DB_ADMIN = 2;
69 
70  // Const for getStdin()
71  public const STDIN_ALL = -1;
72 
78  protected $mParams = [];
79 
81  protected $mShortParamsMap = [];
82 
84  protected $mArgList = [];
85 
87  protected $mOptions = [];
88 
90  protected $mArgs = [];
91 
93  protected $mAllowUnregisteredOptions = false;
94 
96  protected $mSelf;
97 
99  protected $mQuiet = false;
100  protected $mDbUser, $mDbPass;
101 
103  protected $mDescription = '';
104 
106  protected $mInputLoaded = false;
107 
114  protected $mBatchSize = null;
115 
121  private $mGenericParameters = [];
127  private $mDependentParameters = [];
128 
133  private $mDb = null;
134 
136  private $lastReplicationWait = 0.0;
137 
142  public $fileHandle;
143 
145  private $hookContainer;
146 
148  private $hookRunner;
149 
155  private $config;
156 
161  private $requiredExtensions = [];
162 
174  public $orderedOptions = [];
175 
182  public function __construct() {
183  $this->addDefaultParams();
184  register_shutdown_function( [ $this, 'outputChanneled' ], false );
185  }
186 
194  public static function shouldExecute() {
195  global $wgCommandLineMode;
196 
197  if ( !function_exists( 'debug_backtrace' ) ) {
198  // If someone has a better idea...
199  return $wgCommandLineMode;
200  }
201 
202  $bt = debug_backtrace();
203  $count = count( $bt );
204  if ( $count < 2 ) {
205  return false;
206  }
207  if ( $bt[0]['class'] !== self::class || $bt[0]['function'] !== 'shouldExecute' ) {
208  return false; // last call should be to this function
209  }
210  $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ];
211  for ( $i = 1; $i < $count; $i++ ) {
212  if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) {
213  return false; // previous calls should all be "requires"
214  }
215  }
216 
217  return true;
218  }
219 
228  abstract public function execute();
229 
236  protected function supportsOption( $name ) {
237  return isset( $this->mParams[$name] );
238  }
239 
251  protected function addOption( $name, $description, $required = false,
252  $withArg = false, $shortName = false, $multiOccurrence = false
253  ) {
254  $this->mParams[$name] = [
255  'desc' => $description,
256  'require' => $required,
257  'withArg' => $withArg,
258  'shortName' => $shortName,
259  'multiOccurrence' => $multiOccurrence
260  ];
261 
262  if ( $shortName !== false ) {
263  $this->mShortParamsMap[$shortName] = $name;
264  }
265  }
266 
273  protected function hasOption( $name ) {
274  return isset( $this->mOptions[$name] );
275  }
276 
288  protected function getOption( $name, $default = null ) {
289  if ( $this->hasOption( $name ) ) {
290  return $this->mOptions[$name];
291  } else {
292  return $default;
293  }
294  }
295 
302  protected function addArg( $arg, $description, $required = true ) {
303  $this->mArgList[] = [
304  'name' => $arg,
305  'desc' => $description,
306  'require' => $required
307  ];
308  }
309 
314  protected function deleteOption( $name ) {
315  unset( $this->mParams[$name] );
316  }
317 
323  protected function setAllowUnregisteredOptions( $allow ) {
324  $this->mAllowUnregisteredOptions = $allow;
325  }
326 
331  protected function addDescription( $text ) {
332  $this->mDescription = $text;
333  }
334 
340  protected function hasArg( $argId = 0 ) {
341  if ( func_num_args() === 0 ) {
342  wfDeprecated( __METHOD__ . ' without an $argId', '1.33' );
343  }
344 
345  return isset( $this->mArgs[$argId] );
346  }
347 
355  protected function getArg( $argId = 0, $default = null ) {
356  if ( func_num_args() === 0 ) {
357  wfDeprecated( __METHOD__ . ' without an $argId', '1.33' );
358  }
359 
360  return $this->mArgs[$argId] ?? $default;
361  }
362 
370  protected function getBatchSize() {
371  return $this->mBatchSize;
372  }
373 
377  protected function setBatchSize( $s = 0 ) {
378  $this->mBatchSize = $s;
379 
380  // If we support $mBatchSize, show the option.
381  // Used to be in addDefaultParams, but in order for that to
382  // work, subclasses would have to call this function in the constructor
383  // before they called parent::__construct which is just weird
384  // (and really wasn't done).
385  if ( $this->mBatchSize ) {
386  $this->addOption( 'batch-size', 'Run this many operations ' .
387  'per batch, default: ' . $this->mBatchSize, false, true );
388  if ( isset( $this->mParams['batch-size'] ) ) {
389  // This seems a little ugly...
390  $this->mDependentParameters['batch-size'] = $this->mParams['batch-size'];
391  }
392  }
393  }
394 
399  public function getName() {
400  return $this->mSelf;
401  }
402 
409  protected function getStdin( $len = null ) {
410  if ( $len == self::STDIN_ALL ) {
411  return file_get_contents( 'php://stdin' );
412  }
413  $f = fopen( 'php://stdin', 'rt' );
414  if ( !$len ) {
415  return $f;
416  }
417  $input = fgets( $f, $len );
418  fclose( $f );
419 
420  return rtrim( $input );
421  }
422 
426  public function isQuiet() {
427  return $this->mQuiet;
428  }
429 
437  protected function output( $out, $channel = null ) {
438  // This is sometimes called very early, before Setup.php is included.
439  if ( class_exists( MediaWikiServices::class ) ) {
440  // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385)
441  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
442  if ( $stats->getDataCount() > 1000 ) {
443  MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() );
444  }
445  }
446 
447  if ( $this->mQuiet ) {
448  return;
449  }
450  if ( $channel === null ) {
451  $this->cleanupChanneled();
452  print $out;
453  } else {
454  $out = preg_replace( '/\n\z/', '', $out );
455  $this->outputChanneled( $out, $channel );
456  }
457  }
458 
466  protected function error( $err, $die = 0 ) {
467  if ( intval( $die ) !== 0 ) {
468  wfDeprecated( __METHOD__ . '( $err, $die )', '1.31' );
469  $this->fatalError( $err, intval( $die ) );
470  }
471  $this->outputChanneled( false );
472  if (
473  ( PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg' ) &&
474  !defined( 'MW_PHPUNIT_TEST' )
475  ) {
476  fwrite( STDERR, $err . "\n" );
477  } else {
478  print $err;
479  }
480  }
481 
491  protected function fatalError( $msg, $exitCode = 1 ) {
492  $this->error( $msg );
493  exit( $exitCode );
494  }
495 
496  private $atLineStart = true;
497  private $lastChannel = null;
498 
502  public function cleanupChanneled() {
503  if ( !$this->atLineStart ) {
504  print "\n";
505  $this->atLineStart = true;
506  }
507  }
508 
517  public function outputChanneled( $msg, $channel = null ) {
518  if ( $msg === false ) {
519  $this->cleanupChanneled();
520 
521  return;
522  }
523 
524  // End the current line if necessary
525  if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
526  print "\n";
527  }
528 
529  print $msg;
530 
531  $this->atLineStart = false;
532  if ( $channel === null ) {
533  // For unchanneled messages, output trailing newline immediately
534  print "\n";
535  $this->atLineStart = true;
536  }
537  $this->lastChannel = $channel;
538  }
539 
551  public function getDbType() {
552  return self::DB_STD;
553  }
554 
558  protected function addDefaultParams() {
559  # Generic (non-script-dependent) options:
560 
561  $this->addOption( 'help', 'Display this help message', false, false, 'h' );
562  $this->addOption( 'quiet', 'Whether to suppress non-error output', false, false, 'q' );
563  $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true );
564  $this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
565  $this->addOption( 'globals', 'Output globals at the end of processing for debugging' );
566  $this->addOption(
567  'memory-limit',
568  'Set a specific memory limit for the script, '
569  . '"max" for no limit or "default" to avoid changing it',
570  false,
571  true
572  );
573  $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
574  "http://en.wikipedia.org. This is sometimes necessary because " .
575  "server name detection may fail in command line scripts.", false, true );
576  $this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true );
577 
578  # Save generic options to display them separately in help
579  $this->mGenericParameters = $this->mParams;
580 
581  # Script-dependent options:
582 
583  // If we support a DB, show the options
584  if ( $this->getDbType() > 0 ) {
585  $this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
586  $this->addOption( 'dbpass', 'The password to use for this script', false, true );
587  $this->addOption( 'dbgroupdefault', 'The default DB group to use.', false, true );
588  }
589 
590  # Save additional script-dependent options to display
591  # ┬áthem separately in help
592  $this->mDependentParameters = array_diff_key( $this->mParams, $this->mGenericParameters );
593  }
594 
600  public function getConfig() {
601  if ( $this->config === null ) {
602  $this->config = MediaWikiServices::getInstance()->getMainConfig();
603  }
604 
605  return $this->config;
606  }
607 
612  public function setConfig( Config $config ) {
613  $this->config = $config;
614  }
615 
625  protected function requireExtension( $name ) {
626  $this->requiredExtensions[] = $name;
627  }
628 
634  public function checkRequiredExtensions() {
635  $registry = ExtensionRegistry::getInstance();
636  $missing = [];
637  foreach ( $this->requiredExtensions as $name ) {
638  if ( !$registry->isLoaded( $name ) ) {
639  $missing[] = $name;
640  }
641  }
642 
643  if ( $missing ) {
644  if ( count( $missing ) === 1 ) {
645  $msg = 'The "' . $missing[ 0 ] . '" extension must be installed for this script to run. '
646  . 'Please enable it and then try again.';
647  } else {
648  $msg = 'The following extensions must be installed for this script to run: "'
649  . implode( '", "', $missing ) . '". Please enable them and then try again.';
650  }
651  $this->fatalError( $msg );
652  }
653  }
654 
664  public function setAgentAndTriggers() {
665  wfDeprecated( __METHOD__, '1.37' );
666  }
667 
675  public function runChild( $maintClass, $classFile = null ) {
676  // Make sure the class is loaded first
677  if ( !class_exists( $maintClass ) ) {
678  if ( $classFile ) {
679  require_once $classFile;
680  }
681  if ( !class_exists( $maintClass ) ) {
682  $this->fatalError( "Cannot spawn child: $maintClass" );
683  }
684  }
685 
689  $child = new $maintClass();
690  $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
691  if ( $this->mDb !== null ) {
692  $child->setDB( $this->mDb );
693  }
694 
695  return $child;
696  }
697 
701  public function setup() {
702  global $IP, $wgCommandLineMode;
703 
704  # Abort if called from a web server
705  # wfIsCLI() is not available yet
706  if ( PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ) {
707  $this->fatalError( 'This script must be run from the command line' );
708  }
709 
710  if ( $IP === null ) {
711  $this->fatalError( "\$IP not set, aborting!\n" .
712  '(Did you forget to call parent::__construct() in your maintenance script?)' );
713  }
714 
715  # Make sure we can handle script parameters
716  if ( !ini_get( 'register_argc_argv' ) ) {
717  $this->fatalError( 'Cannot get command line arguments, register_argc_argv is set to false' );
718  }
719 
720  // Send PHP warnings and errors to stderr instead of stdout.
721  // This aids in diagnosing problems, while keeping messages
722  // out of redirected output.
723  if ( ini_get( 'display_errors' ) ) {
724  ini_set( 'display_errors', 'stderr' );
725  }
726 
727  $this->loadParamsAndArgs();
728 
729  # Set the memory limit
730  # Note we need to set it again later in case LocalSettings changed it
731  $this->adjustMemoryLimit();
732 
733  # Set max execution time to 0 (no limit). PHP.net says that
734  # "When running PHP from the command line the default setting is 0."
735  # But sometimes this doesn't seem to be the case.
736  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal Scalar okay with php8.1
737  ini_set( 'max_execution_time', 0 );
738 
739  $wgCommandLineMode = true;
740 
741  # Turn off output buffering if it's on
742  while ( ob_get_level() > 0 ) {
743  ob_end_flush();
744  }
745  }
746 
757  public function memoryLimit() {
758  $limit = $this->getOption( 'memory-limit', 'max' );
759  $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
760  return $limit;
761  }
762 
766  protected function adjustMemoryLimit() {
767  $limit = $this->memoryLimit();
768  if ( $limit == 'max' ) {
769  $limit = -1; // no memory limit
770  }
771  if ( $limit != 'default' ) {
772  ini_set( 'memory_limit', $limit );
773  }
774  }
775 
779  protected function activateProfiler() {
781 
782  $output = $this->getOption( 'profiler' );
783  if ( !$output ) {
784  return;
785  }
786 
787  if ( isset( $wgProfiler['class'] ) ) {
788  $class = $wgProfiler['class'];
790  $profiler = new $class(
791  [ 'sampling' => 1, 'output' => [ $output ] ]
792  + $wgProfiler
793  + [ 'threshold' => 0.0 ]
794  );
795  $profiler->setAllowOutput();
796  Profiler::replaceStubInstance( $profiler );
797  }
798 
799  $trxProfiler = Profiler::instance()->getTransactionProfiler();
800  $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
801  $trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
802  }
803 
807  public function clearParamsAndArgs() {
808  $this->mOptions = [];
809  $this->mArgs = [];
810  $this->mInputLoaded = false;
811  }
812 
820  public function loadWithArgv( $argv ) {
821  $options = [];
822  $args = [];
823  $this->orderedOptions = [];
824 
825  # Parse arguments
826  for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
827  if ( $arg == '--' ) {
828  # End of options, remainder should be considered arguments
829  $arg = next( $argv );
830  while ( $arg !== false ) {
831  $args[] = $arg;
832  $arg = next( $argv );
833  }
834  break;
835  } elseif ( substr( $arg, 0, 2 ) == '--' ) {
836  # Long options
837  $option = substr( $arg, 2 );
838  if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
839  $param = next( $argv );
840  if ( $param === false ) {
841  $this->error( "\nERROR: $option parameter needs a value after it\n" );
842  $this->maybeHelp( true );
843  }
844 
845  $this->setParam( $options, $option, $param );
846  } else {
847  $bits = explode( '=', $option, 2 );
848  $this->setParam( $options, $bits[0], $bits[1] ?? 1 );
849  }
850  } elseif ( $arg == '-' ) {
851  # Lonely "-", often used to indicate stdin or stdout.
852  $args[] = $arg;
853  } elseif ( substr( $arg, 0, 1 ) == '-' ) {
854  # Short options
855  $argLength = strlen( $arg );
856  for ( $p = 1; $p < $argLength; $p++ ) {
857  $option = $arg[$p];
858  if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
859  $option = $this->mShortParamsMap[$option];
860  }
861 
862  if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
863  $param = next( $argv );
864  if ( $param === false ) {
865  $this->error( "\nERROR: $option parameter needs a value after it\n" );
866  $this->maybeHelp( true );
867  }
868  $this->setParam( $options, $option, $param );
869  } else {
870  $this->setParam( $options, $option, 1 );
871  }
872  }
873  } else {
874  $args[] = $arg;
875  }
876  }
877 
878  $this->mOptions = $options;
879  $this->mArgs = $args;
880  $this->loadSpecialVars();
881  $this->mInputLoaded = true;
882  }
883 
896  private function setParam( &$options, $option, $value ) {
897  $this->orderedOptions[] = [ $option, $value ];
898 
899  if ( isset( $this->mParams[$option] ) ) {
900  $multi = $this->mParams[$option]['multiOccurrence'];
901  } else {
902  $multi = false;
903  }
904  $exists = array_key_exists( $option, $options );
905  if ( $multi && $exists ) {
906  $options[$option][] = $value;
907  } elseif ( $multi ) {
908  $options[$option] = [ $value ];
909  } elseif ( !$exists ) {
910  $options[$option] = $value;
911  } else {
912  $this->error( "\nERROR: $option parameter given twice\n" );
913  $this->maybeHelp( true );
914  }
915  }
916 
926  public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
927  # If we were given opts or args, set those and return early
928  if ( $self !== null ) {
929  $this->mSelf = $self;
930  $this->mInputLoaded = true;
931  }
932  if ( $opts !== null ) {
933  $this->mOptions = $opts;
934  $this->mInputLoaded = true;
935  }
936  if ( $args !== null ) {
937  $this->mArgs = $args;
938  $this->mInputLoaded = true;
939  }
940 
941  # If we've already loaded input (either by user values or from $argv)
942  # skip on loading it again. The array_shift() will corrupt values if
943  # it's run again and again
944  if ( $this->mInputLoaded ) {
945  $this->loadSpecialVars();
946 
947  return;
948  }
949 
950  global $argv;
951  $this->mSelf = $argv[0];
952  $this->loadWithArgv( array_slice( $argv, 1 ) );
953  }
954 
959  public function validateParamsAndArgs() {
960  $die = false;
961  # Check to make sure we've got all the required options
962  foreach ( $this->mParams as $opt => $info ) {
963  if ( $info['require'] && !$this->hasOption( $opt ) ) {
964  $this->error( "Param $opt required!" );
965  $die = true;
966  }
967  }
968  # Check arg list too
969  foreach ( $this->mArgList as $k => $info ) {
970  if ( $info['require'] && !$this->hasArg( $k ) ) {
971  $this->error( 'Argument <' . $info['name'] . '> required!' );
972  $die = true;
973  }
974  }
975  if ( !$this->mAllowUnregisteredOptions ) {
976  # Check for unexpected options
977  foreach ( $this->mOptions as $opt => $val ) {
978  if ( !$this->supportsOption( $opt ) ) {
979  $this->error( "Unexpected option $opt!" );
980  $die = true;
981  }
982  }
983  }
984 
985  $this->maybeHelp( $die );
986  }
987 
992  protected function loadSpecialVars() {
993  if ( $this->hasOption( 'dbuser' ) ) {
994  $this->mDbUser = $this->getOption( 'dbuser' );
995  }
996  if ( $this->hasOption( 'dbpass' ) ) {
997  $this->mDbPass = $this->getOption( 'dbpass' );
998  }
999  if ( $this->hasOption( 'quiet' ) ) {
1000  $this->mQuiet = true;
1001  }
1002  if ( $this->hasOption( 'batch-size' ) ) {
1003  $this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
1004  }
1005  }
1006 
1012  protected function maybeHelp( $force = false ) {
1013  if ( !$force && !$this->hasOption( 'help' ) ) {
1014  return;
1015  }
1016  $this->showHelp();
1017  die( 1 );
1018  }
1019 
1023  protected function showHelp() {
1024  $screenWidth = 80; // TODO: Calculate this!
1025  $tab = " ";
1026  $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
1027 
1028  ksort( $this->mParams );
1029  $this->mQuiet = false;
1030 
1031  // Description ...
1032  if ( $this->mDescription ) {
1033  $this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
1034  }
1035  $output = "\nUsage: php " . basename( $this->mSelf );
1036 
1037  // ... append parameters ...
1038  if ( $this->mParams ) {
1039  $output .= " [--" . implode( "|--", array_keys( $this->mParams ) ) . "]";
1040  }
1041 
1042  // ... and append arguments.
1043  if ( $this->mArgList ) {
1044  $output .= ' ';
1045  foreach ( $this->mArgList as $k => $arg ) {
1046  if ( $arg['require'] ) {
1047  $output .= '<' . $arg['name'] . '>';
1048  } else {
1049  $output .= '[' . $arg['name'] . ']';
1050  }
1051  if ( $k < count( $this->mArgList ) - 1 ) {
1052  $output .= ' ';
1053  }
1054  }
1055  }
1056  $this->output( "$output\n\n" );
1057 
1058  $this->formatHelpItems(
1059  $this->mGenericParameters,
1060  'Generic maintenance parameters',
1061  $descWidth, $tab
1062  );
1063 
1064  $this->formatHelpItems(
1065  $this->mDependentParameters,
1066  'Script dependent parameters',
1067  $descWidth, $tab
1068  );
1069 
1070  // Script-specific parameters not defined on construction by
1071  // Maintenance::addDefaultParams()
1072  $scriptSpecificParams = array_diff_key(
1073  # all script parameters:
1074  $this->mParams,
1075  # remove the Maintenance default parameters:
1076  $this->mGenericParameters,
1077  $this->mDependentParameters
1078  );
1079 
1080  $this->formatHelpItems(
1081  $scriptSpecificParams,
1082  'Script specific parameters',
1083  $descWidth, $tab
1084  );
1085 
1086  // Print arguments
1087  if ( count( $this->mArgList ) > 0 ) {
1088  $this->output( "Arguments:\n" );
1089  // Arguments description
1090  foreach ( $this->mArgList as $info ) {
1091  $openChar = $info['require'] ? '<' : '[';
1092  $closeChar = $info['require'] ? '>' : ']';
1093  $this->output(
1094  wordwrap(
1095  "$tab$openChar" . $info['name'] . "$closeChar: " . $info['desc'],
1096  $descWidth,
1097  "\n$tab$tab"
1098  ) . "\n"
1099  );
1100  }
1101  $this->output( "\n" );
1102  }
1103  }
1104 
1105  private function formatHelpItems( array $items, $heading, $descWidth, $tab ) {
1106  if ( $items === [] ) {
1107  return;
1108  }
1109 
1110  $this->output( "$heading:\n" );
1111 
1112  foreach ( $items as $name => $info ) {
1113  if ( $info['shortName'] !== false ) {
1114  $name .= ' (-' . $info['shortName'] . ')';
1115  }
1116  $this->output(
1117  wordwrap(
1118  "$tab--$name: " . strtr( $info['desc'], [ "\n" => "\n$tab$tab" ] ),
1119  $descWidth,
1120  "\n$tab$tab"
1121  ) . "\n"
1122  );
1123  }
1124 
1125  $this->output( "\n" );
1126  }
1127 
1135  public function finalSetup( SettingsBuilder $settingsBuilder = null ) {
1136  if ( !$settingsBuilder ) {
1137  // HACK for backwards compatibility. All subclasses that override
1138  // finalSetup() should be updated to pass $settingsBuilder along.
1139  // XXX: We don't want the parameter to be nullable! How can we make it required
1140  // without breaking backwards compatibility?
1141  $settingsBuilder = $GLOBALS['wgSettings'];
1142  }
1143 
1144  $config = $settingsBuilder->getConfig();
1145  $overrides = [];
1146  $overrides['DBadminuser'] = $config->get( MainConfigNames::DBadminuser );
1147  $overrides['DBadminpassword'] = $config->get( MainConfigNames::DBadminpassword );
1148 
1149  # Turn off output buffering again, it might have been turned on in the settings files
1150  if ( ob_get_level() ) {
1151  ob_end_flush();
1152  }
1153  # Same with these
1154  $overrides['CommandLineMode'] = true;
1155 
1156  # Override $wgServer
1157  if ( $this->hasOption( 'server' ) ) {
1158  $overrides['Server'] = $this->getOption( 'server', $config->get( MainConfigNames::Server ) );
1159  }
1160 
1161  # If these were passed, use them
1162  if ( $this->mDbUser ) {
1163  $overrides['DBadminuser'] = $this->mDbUser;
1164  }
1165  if ( $this->mDbPass ) {
1166  $overrides['DBadminpassword'] = $this->mDbPass;
1167  }
1168  if ( $this->hasOption( 'dbgroupdefault' ) ) {
1169  $overrides['DBDefaultGroup'] = $this->getOption( 'dbgroupdefault', null );
1170  // TODO: once MediaWikiServices::getInstance() starts throwing exceptions
1171  // and not deprecation warnings for premature access to service container,
1172  // we can remove this line. This method is called before Setup.php,
1173  // so it would be guaranteed DBLoadBalancerFactory is not yet initialized.
1174  if ( MediaWikiServices::hasInstance() ) {
1175  $service = MediaWikiServices::getInstance()->peekService( 'DBLoadBalancerFactory' );
1176  if ( $service ) {
1177  $service->destroy();
1178  }
1179  }
1180  }
1181 
1182  if ( $this->getDbType() == self::DB_ADMIN && isset( $overrides[ 'DBadminuser' ] ) ) {
1183  $overrides['DBuser'] = $overrides[ 'DBadminuser' ];
1184  $overrides['DBpassword'] = $overrides[ 'DBadminpassword' ];
1185 
1187  $dbServers = $config->get( MainConfigNames::DBservers );
1188  if ( $dbServers ) {
1189  foreach ( $dbServers as $i => $server ) {
1190  $dbServers[$i]['user'] = $overrides['DBuser'];
1191  $dbServers[$i]['password'] = $overrides['DBpassword'];
1192  }
1193  $overrides['DBservers'] = $dbServers;
1194  }
1195 
1196  $lbFactoryConf = $config->get( MainConfigNames::LBFactoryConf );
1197  if ( isset( $lbFactoryConf['serverTemplate'] ) ) {
1198  $lbFactoryConf['serverTemplate']['user'] = $overrides['DBuser'];
1199  $lbFactoryConf['serverTemplate']['password'] = $overrides['DBpassword'];
1200  $overrides['LBFactoryConf'] = $lbFactoryConf;
1201  }
1202 
1203  // TODO: once MediaWikiServices::getInstance() starts throwing exceptions
1204  // and not deprecation warnings for premature access to service container,
1205  // we can remove this line. This method is called before Setup.php,
1206  // so it would be guaranteed DBLoadBalancerFactory is not yet initialized.
1207  if ( MediaWikiServices::hasInstance() ) {
1208  $service = MediaWikiServices::getInstance()->peekService( 'DBLoadBalancerFactory' );
1209  if ( $service ) {
1210  $service->destroy();
1211  }
1212  }
1213  }
1214 
1215  // Per-script profiling; useful for debugging
1216  $this->activateProfiler();
1217 
1218  $this->afterFinalSetup();
1219 
1220  $overrides['ShowExceptionDetails'] = true;
1221  $overrides['ShowHostname'] = true;
1222 
1223  $ini = [
1224  'max_execution_time' => 0,
1225  ];
1226 
1227  $this->adjustMemoryLimit();
1228 
1229  $settingsBuilder->loadArray( [ 'config' => $overrides, 'php-ini' => $ini ] );
1230  }
1231 
1236  protected function afterFinalSetup() {
1237  }
1238 
1243  public function globals() {
1244  if ( $this->hasOption( 'globals' ) ) {
1245  print_r( $GLOBALS );
1246  }
1247  }
1248 
1258  public function shutdown() {
1259  $lbFactory = null;
1260  if (
1261  $this->getDbType() !== self::DB_NONE &&
1262  // Service might be disabled, e.g. when running install.php
1263  !MediaWikiServices::getInstance()->isServiceDisabled( 'DBLoadBalancerFactory' )
1264  ) {
1265  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1266  if ( $lbFactory->isReadyForRoundOperations() ) {
1267  $lbFactory->commitPrimaryChanges( get_class( $this ) );
1268  }
1269 
1271  }
1272 
1273  // Handle external profiler outputs
1274  // FIXME: Handle embedded outputs as well, such as ProfilerOutputText (T253547)
1275  $profiler = Profiler::instance();
1276  $profiler->logData();
1277 
1279  MediaWikiServices::getInstance()->getStatsdDataFactory(),
1280  $this->getConfig()
1281  );
1282 
1283  if ( $lbFactory ) {
1284  if ( $lbFactory->isReadyForRoundOperations() ) {
1285  $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT );
1286  }
1287  }
1288  }
1289 
1294  public function loadSettings() {
1295  global $wgCommandLineMode;
1296 
1297  if ( isset( $this->mOptions['conf'] ) ) {
1298  // Define the constant instead of directly setting $settingsFile
1299  // to ensure consistency. wfDetectLocalSettingsFile() will return
1300  // MW_CONFIG_FILE if it is defined.
1301  define( 'MW_CONFIG_FILE', $this->mOptions['conf'] );
1302  }
1303  $settingsFile = wfDetectLocalSettingsFile();
1304 
1305  if ( isset( $this->mOptions['wiki'] ) ) {
1306  $wikiName = $this->mOptions['wiki'];
1307  $bits = explode( '-', $wikiName, 2 );
1308  define( 'MW_DB', $bits[0] );
1309  define( 'MW_PREFIX', $bits[1] ?? '' );
1310  define( 'MW_WIKI_NAME', $wikiName );
1311  } elseif ( isset( $this->mOptions['server'] ) ) {
1312  // Provide the option for site admins to detect and configure
1313  // multiple wikis based on server names. This offers --server
1314  // as alternative to --wiki.
1315  // See https://www.mediawiki.org/wiki/Manual:Wiki_family
1316  $_SERVER['SERVER_NAME'] = $this->mOptions['server'];
1317  }
1318 
1319  if ( !is_readable( $settingsFile ) ) {
1320  $this->fatalError( "The file $settingsFile must exist and be readable.\n" .
1321  "Use --conf to specify it." );
1322  }
1323  $wgCommandLineMode = true;
1324 
1325  return $settingsFile;
1326  }
1327 
1333  public function purgeRedundantText( $delete = true ) {
1334  # Data should come off the master, wrapped in a transaction
1335  $dbw = $this->getDB( DB_PRIMARY );
1336  $this->beginTransaction( $dbw, __METHOD__ );
1337 
1338  # Get "active" text records via the content table
1339  $cur = [];
1340  $this->output( 'Searching for active text records via contents table...' );
1341  $res = $dbw->select( 'content', 'content_address', [], __METHOD__, [ 'DISTINCT' ] );
1342  $blobStore = MediaWikiServices::getInstance()->getBlobStore();
1343  foreach ( $res as $row ) {
1344  // @phan-suppress-next-line PhanUndeclaredMethod
1345  $textId = $blobStore->getTextIdFromAddress( $row->content_address );
1346  if ( $textId ) {
1347  $cur[] = $textId;
1348  }
1349  }
1350  $this->output( "done.\n" );
1351 
1352  # Get the IDs of all text records not in these sets
1353  $this->output( 'Searching for inactive text records...' );
1354  $cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1355  $res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1356  $old = [];
1357  foreach ( $res as $row ) {
1358  $old[] = $row->old_id;
1359  }
1360  $this->output( "done.\n" );
1361 
1362  # Inform the user of what we're going to do
1363  $count = count( $old );
1364  $this->output( "$count inactive items found.\n" );
1365 
1366  # Delete as appropriate
1367  if ( $delete && $count ) {
1368  $this->output( 'Deleting...' );
1369  $dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1370  $this->output( "done.\n" );
1371  }
1372 
1373  $this->commitTransaction( $dbw, __METHOD__ );
1374  }
1375 
1380  protected function getDir() {
1381  return __DIR__ . '/../';
1382  }
1383 
1398  protected function getDB( $db, $groups = [], $dbDomain = false ) {
1399  if ( $this->mDb === null ) {
1400  return MediaWikiServices::getInstance()
1401  ->getDBLoadBalancerFactory()
1402  ->getMainLB( $dbDomain )
1403  ->getMaintenanceConnectionRef( $db, $groups, $dbDomain );
1404  }
1405 
1406  return $this->mDb;
1407  }
1408 
1415  public function setDB( IMaintainableDatabase $db ) {
1416  $this->mDb = $db;
1417  }
1418 
1429  protected function beginTransaction( IDatabase $dbw, $fname ) {
1430  $dbw->begin( $fname );
1431  }
1432 
1444  protected function commitTransaction( IDatabase $dbw, $fname ) {
1445  $dbw->commit( $fname );
1446  return $this->waitForReplication();
1447  }
1448 
1455  protected function waitForReplication() {
1456  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1457  $waitSucceeded = $lbFactory->waitForReplication(
1458  [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1459  );
1460  $this->lastReplicationWait = microtime( true );
1461  return $waitSucceeded;
1462  }
1463 
1474  protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1475  $dbw->rollback( $fname );
1476  }
1477 
1488  protected function countDown( $seconds ) {
1489  if ( $this->isQuiet() ) {
1490  return;
1491  }
1492  for ( $i = $seconds; $i >= 0; $i-- ) {
1493  if ( $i != $seconds ) {
1494  $this->output( str_repeat( "\x08", strlen( (string)( $i + 1 ) ) ) );
1495  }
1496  $this->output( (string)$i );
1497  if ( $i ) {
1498  sleep( 1 );
1499  }
1500  }
1501  $this->output( "\n" );
1502  }
1503 
1512  public static function posix_isatty( $fd ) {
1513  if ( !function_exists( 'posix_isatty' ) ) {
1514  return !$fd;
1515  }
1516 
1517  return posix_isatty( $fd );
1518  }
1519 
1525  public static function readconsole( $prompt = '> ' ) {
1526  static $isatty = null;
1527  if ( $isatty === null ) {
1528  $isatty = self::posix_isatty( 0 /*STDIN*/ );
1529  }
1530 
1531  if ( $isatty && function_exists( 'readline' ) ) {
1532  return readline( $prompt );
1533  }
1534 
1535  if ( $isatty ) {
1536  $st = self::readlineEmulation( $prompt );
1537  } elseif ( feof( STDIN ) ) {
1538  $st = false;
1539  } else {
1540  $st = fgets( STDIN, 1024 );
1541  }
1542  if ( $st === false ) {
1543  return false;
1544  }
1545 
1546  return trim( $st );
1547  }
1548 
1554  private static function readlineEmulation( $prompt ) {
1555  $bash = ExecutableFinder::findInDefaultPaths( 'bash' );
1556  if ( !wfIsWindows() && $bash ) {
1557  $encPrompt = Shell::escape( $prompt );
1558  $command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1559  $result = Shell::command( $bash, '-c', $command )
1560  ->passStdin()
1561  ->forwardStderr()
1562  ->execute();
1563 
1564  if ( $result->getExitCode() == 0 ) {
1565  return $result->getStdout();
1566  }
1567 
1568  if ( $result->getExitCode() == 127 ) {
1569  // Couldn't execute bash even though we thought we saw it.
1570  // Shell probably spit out an error message, sorry :(
1571  // Fall through to fgets()...
1572  } else {
1573  // EOF/ctrl+D
1574  return false;
1575  }
1576  }
1577 
1578  // Fallback... we'll have no editing controls, EWWW
1579  if ( feof( STDIN ) ) {
1580  return false;
1581  }
1582  print $prompt;
1583 
1584  return fgets( STDIN, 1024 );
1585  }
1586 
1594  public static function getTermSize() {
1595  static $termSize = null;
1596 
1597  if ( $termSize !== null ) {
1598  return $termSize;
1599  }
1600 
1601  $default = [ 80, 50 ];
1602 
1603  if ( wfIsWindows() || Shell::isDisabled() ) {
1604  $termSize = $default;
1605 
1606  return $termSize;
1607  }
1608 
1609  // It's possible to get the screen size with VT-100 terminal escapes,
1610  // but reading the responses is not possible without setting raw mode
1611  // (unless you want to require the user to press enter), and that
1612  // requires an ioctl(), which we can't do. So we have to shell out to
1613  // something that can do the relevant syscalls. There are a few
1614  // options. Linux and Mac OS X both have "stty size" which does the
1615  // job directly.
1616  $result = Shell::command( 'stty', 'size' )->passStdin()->execute();
1617  if ( $result->getExitCode() !== 0 ||
1618  !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m )
1619  ) {
1620  $termSize = $default;
1621 
1622  return $termSize;
1623  }
1624 
1625  $termSize = [ intval( $m[2] ), intval( $m[1] ) ];
1626 
1627  return $termSize;
1628  }
1629 
1634  public static function requireTestsAutoloader() {
1635  require_once __DIR__ . '/../../tests/common/TestsAutoLoader.php';
1636  }
1637 
1644  protected function getHookContainer() {
1645  if ( !$this->hookContainer ) {
1646  $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1647  }
1648  return $this->hookContainer;
1649  }
1650 
1659  protected function getHookRunner() {
1660  if ( !$this->hookRunner ) {
1661  $this->hookRunner = new HookRunner( $this->getHookContainer() );
1662  }
1663  return $this->hookRunner;
1664  }
1665 
1676  protected function parseIntList( $text ) {
1677  $ids = preg_split( '/[\s,;:|]+/', $text );
1678  $ids = array_map(
1679  static function ( $id ) {
1680  return (int)$id;
1681  },
1682  $ids
1683  );
1684  return array_filter( $ids );
1685  }
1686 
1695  protected function validateUserOption( $errorMsg ) {
1696  $user = null;
1697  if ( $this->hasOption( "user" ) ) {
1698  $user = User::newFromName( $this->getOption( 'user' ) );
1699  } elseif ( $this->hasOption( "userid" ) ) {
1700  $user = User::newFromId( $this->getOption( 'userid' ) );
1701  } else {
1702  $this->fatalError( $errorMsg );
1703  }
1704  if ( !$user || !$user->getId() ) {
1705  if ( $this->hasOption( "user" ) ) {
1706  $this->fatalError( "No such user: " . $this->getOption( 'user' ) );
1707  } elseif ( $this->hasOption( "userid" ) ) {
1708  $this->fatalError( "No such user id: " . $this->getOption( 'userid' ) );
1709  }
1710  }
1711 
1712  // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
1713  return $user;
1714  }
1715 }
wfDetectLocalSettingsFile(?string $installationPath=null)
Decide and remember where to load LocalSettings from.
array $wgProfiler
Variable for the Profiler setting, for use in LocalSettings.php.
array $wgTrxProfilerLimits
Variable for the TrxProfilerLimits setting, for use in LocalSettings.php.
global $wgCommandLineMode
wfIsWindows()
Check if the operating system is Windows.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
string false $maintClass
Definition: Maintenance.php:57
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:90
static doUpdates( $mode='run', $stage=self::ALL)
Consume and execute all pending updates.
static findInDefaultPaths( $names, $versionInfo=false)
Same as locateExecutable(), but checks in getPossibleBinPaths() by default.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:61
getDB( $db, $groups=[], $dbDomain=false)
Returns a database to be used by current maintenance script.
setup()
Do some checking and basic setup.
array[] $mParams
Array of desired/allowed params.
Definition: Maintenance.php:78
__construct()
Default constructor.
error( $err, $die=0)
Throw an error to the user.
const STDIN_ALL
Definition: Maintenance.php:71
getName()
Get the script's name.
array[] $mGenericParameters
Generic options added by addDefaultParams()
addArg( $arg, $description, $required=true)
Add some args that are needed.
requireExtension( $name)
Indicate that the specified extension must be loaded before the script can run.
showHelp()
Definitely show the help.
int null $mBatchSize
Batch size.
setAgentAndTriggers()
This method used to be for internal use by doMaintenance.php to apply some optional global state to L...
setAllowUnregisteredOptions( $allow)
Sets whether to allow unregistered options, which are options passed to a script that do not match an...
beginTransaction(IDatabase $dbw, $fname)
Begin a transaction on a DB.
static getTermSize()
Get the terminal size as a two-element array where the first element is the width (number of columns)...
HookContainer null $hookContainer
clearParamsAndArgs()
Clear all params and arguments.
array $requiredExtensions
setParam(&$options, $option, $value)
Helper function used solely by loadParamsAndArgs to prevent code duplication.
const DB_NONE
Constants for DB access type.
Definition: Maintenance.php:66
commitTransaction(IDatabase $dbw, $fname)
Commit the transaction on a DB handle and wait for replica DBs to catch up.
output( $out, $channel=null)
Throw some output to the user.
supportsOption( $name)
Checks to see if a particular option in supported.
array[] $mDependentParameters
Generic options which might or not be supported by the script.
getStdin( $len=null)
Return input from stdin.
const DB_STD
Definition: Maintenance.php:67
cleanupChanneled()
Clean up channeled output.
memoryLimit()
Normally we disable the memory_limit when running admin scripts.
array $mArgList
Desired/allowed args.
Definition: Maintenance.php:84
getHookRunner()
Get a HookRunner for running core hooks.
afterFinalSetup()
Override to perform any required operation at the end of initialisation.
finalSetup(SettingsBuilder $settingsBuilder=null)
Handle some last-minute setup here.
hasArg( $argId=0)
Does a given argument exist?
const DB_ADMIN
Definition: Maintenance.php:68
getDir()
Get the maintenance directory.
addDefaultParams()
Add the default parameters to the scripts.
bool $mInputLoaded
Have we already loaded our user input?
deleteOption( $name)
Remove an option.
static readlineEmulation( $prompt)
Emulate readline()
static requireTestsAutoloader()
Call this to set up the autoloader to allow classes to be used from the tests directory.
loadParamsAndArgs( $self=null, $opts=null, $args=null)
Process command line arguments $mOptions becomes an array with keys set to the option names $mArgs be...
waitForReplication()
Wait for replica DBs to catch up.
outputChanneled( $msg, $channel=null)
Message outputter with channeled message support.
resource null $fileHandle
Used when creating separate schema files.
loadSpecialVars()
Handle the special variables that are global to all scripts @stable to override.
setDB(IMaintainableDatabase $db)
Sets database object to be returned by getDB().
float $lastReplicationWait
UNIX timestamp.
array $orderedOptions
Used to read the options in the order they were passed.
loadSettings()
Generic setup for most installs.
Config null $config
Accessible via getConfig()
hasOption( $name)
Checks to see if a particular option was set.
purgeRedundantText( $delete=true)
Support function for cleaning up redundant text records.
countDown( $seconds)
Count down from $seconds to zero on the terminal, with a one-second pause between showing each number...
runChild( $maintClass, $classFile=null)
Run a child maintenance script.
IMaintainableDatabase null $mDb
Used by getDB() / setDB()
array $mOptions
This is the list of options that were actually passed.
Definition: Maintenance.php:87
execute()
Do the actual work.
static readconsole( $prompt='> ')
Prompt the console for input.
static posix_isatty( $fd)
Wrapper for posix_isatty() We default as considering stdin a tty (for nice readline methods) but trea...
adjustMemoryLimit()
Adjusts PHP's memory limit to better suit our needs, if needed.
validateParamsAndArgs()
Run some validation checks on the params, etc.
getHookContainer()
Get a HookContainer, for running extension hooks or for hook metadata.
HookRunner null $hookRunner
validateUserOption( $errorMsg)
getDbType()
Does the script need different DB access? By default, we give Maintenance scripts normal rights to th...
getBatchSize()
Returns batch size.
bool $mQuiet
Special vars for params that are always used.
Definition: Maintenance.php:99
parseIntList( $text)
Utility function to parse a string (perhaps from a command line option) into a list of integers (perh...
getArg( $argId=0, $default=null)
Get an argument.
addDescription( $text)
Set the description text.
activateProfiler()
Activate the profiler (assuming $wgProfiler is set)
shutdown()
Call before exiting CLI process for the last DB commit, and flush any remaining buffers and other def...
maybeHelp( $force=false)
Maybe show the help.
bool $mAllowUnregisteredOptions
Allow arbitrary options to be passed, or only specified ones?
Definition: Maintenance.php:93
loadWithArgv( $argv)
Load params and arguments from a given array of command-line arguments.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
string null $mSelf
Name of the script currently running.
Definition: Maintenance.php:96
static shouldExecute()
Should we execute the maintenance script, or just allow it to be included as a standalone class?...
getOption( $name, $default=null)
Get an option, or return the default.
array $mShortParamsMap
Mapping short parameters to long ones.
Definition: Maintenance.php:81
checkRequiredExtensions()
Verify that the required extensions are installed.
rollbackTransaction(IDatabase $dbw, $fname)
Rollback the transaction on a DB handle.
string $mDescription
A description of the script, children should change this via addDescription()
globals()
Potentially debug globals.
setConfig(Config $config)
setBatchSize( $s=0)
array $mArgs
This is the list of arguments that were actually passed.
Definition: Maintenance.php:90
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
formatHelpItems(array $items, $heading, $descWidth, $tab)
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Utility for loading settings files.
Executes shell commands.
Definition: Shell.php:46
static emitBufferedStatsdData(IBufferingStatsdDataFactory $stats, Config $config)
Send out any buffered statsd data according to sampling rules.
Definition: MediaWiki.php:1172
static replaceStubInstance(Profiler $profiler)
Replace the current profiler with $profiler if no non-stub profiler is set.
Definition: Profiler.php:105
static instance()
Singleton.
Definition: Profiler.php:69
static newFromName( $name, $validate='valid')
Definition: User.php:599
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:640
$self
Interface for configuration instances.
Definition: Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:40
rollback( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Rollback a transaction previously started using begin()
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin()
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
Advanced database interface for IDatabase handles that include maintenance methods.
$command
Definition: mcc.php:125
if( $line===false) $args
Definition: mcc.php:124
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
const DB_PRIMARY
Definition: defines.php:27