MediaWiki  master
Maintenance.php
Go to the documentation of this file.
1 <?php
23 // Bail on old versions of PHP, or if composer has not been run yet to install
24 // dependencies.
25 require_once __DIR__ . '/../includes/PHPVersionCheck.php';
26 wfEntryPointCheck( 'cli' );
27 
33 // Define this so scripts can easily find doMaintenance.php
34 define( 'RUN_MAINTENANCE_IF_MAIN', __DIR__ . '/doMaintenance.php' );
35 define( 'DO_MAINTENANCE', RUN_MAINTENANCE_IF_MAIN ); // original name, harmless
36 
37 $maintClass = false;
38 
40 
51 abstract class Maintenance {
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 = [];
65 
66  // Array of mapping short parameters to long ones
67  protected $mShortParamsMap = [];
68 
69  // Array of desired args
70  protected $mArgList = [];
71 
72  // This is the list of options that were actually passed
73  protected $mOptions = [];
74 
75  // This is the list of arguments that were actually passed
76  protected $mArgs = [];
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 via addDescription()
86  protected $mDescription = '';
87 
88  // Have we already loaded our user input?
89  protected $mInputLoaded = false;
90 
97  protected $mBatchSize = null;
98 
99  // Generic options added by addDefaultParams()
100  private $mGenericParameters = [];
101  // Generic options which might or not be supported by the script
102  private $mDependantParameters = [];
103 
108  private $mDb = null;
109 
111  private $lastSlaveWait = 0.0;
112 
117  public $fileHandle;
118 
124  private $config;
125 
130  private $requiredExtensions = [];
131 
143  public $orderedOptions = [];
144 
149  public function __construct() {
150  // Setup $IP, using MW_INSTALL_PATH if it exists
151  global $IP;
152  $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
153  ? getenv( 'MW_INSTALL_PATH' )
154  : realpath( __DIR__ . '/..' );
155 
156  $this->addDefaultParams();
157  register_shutdown_function( [ $this, 'outputChanneled' ], false );
158  }
159 
167  public static function shouldExecute() {
169 
170  if ( !function_exists( 'debug_backtrace' ) ) {
171  // If someone has a better idea...
172  return $wgCommandLineMode;
173  }
174 
175  $bt = debug_backtrace();
176  $count = count( $bt );
177  if ( $count < 2 ) {
178  return false; // sanity
179  }
180  if ( $bt[0]['class'] !== 'Maintenance' || $bt[0]['function'] !== 'shouldExecute' ) {
181  return false; // last call should be to this function
182  }
183  $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ];
184  for ( $i = 1; $i < $count; $i++ ) {
185  if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) {
186  return false; // previous calls should all be "requires"
187  }
188  }
189 
190  return true;
191  }
192 
196  abstract public function execute();
197 
209  protected function addOption( $name, $description, $required = false,
210  $withArg = false, $shortName = false, $multiOccurrence = false
211  ) {
212  $this->mParams[$name] = [
213  'desc' => $description,
214  'require' => $required,
215  'withArg' => $withArg,
216  'shortName' => $shortName,
217  'multiOccurrence' => $multiOccurrence
218  ];
219 
220  if ( $shortName !== false ) {
221  $this->mShortParamsMap[$shortName] = $name;
222  }
223  }
224 
230  protected function hasOption( $name ) {
231  return isset( $this->mOptions[$name] );
232  }
233 
244  protected function getOption( $name, $default = null ) {
245  if ( $this->hasOption( $name ) ) {
246  return $this->mOptions[$name];
247  } else {
248  // Set it so we don't have to provide the default again
249  $this->mOptions[$name] = $default;
250 
251  return $this->mOptions[$name];
252  }
253  }
254 
261  protected function addArg( $arg, $description, $required = true ) {
262  $this->mArgList[] = [
263  'name' => $arg,
264  'desc' => $description,
265  'require' => $required
266  ];
267  }
268 
273  protected function deleteOption( $name ) {
274  unset( $this->mParams[$name] );
275  }
276 
281  protected function addDescription( $text ) {
282  $this->mDescription = $text;
283  }
284 
290  protected function hasArg( $argId = 0 ) {
291  return isset( $this->mArgs[$argId] );
292  }
293 
300  protected function getArg( $argId = 0, $default = null ) {
301  return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default;
302  }
303 
308  protected function setBatchSize( $s = 0 ) {
309  $this->mBatchSize = $s;
310 
311  // If we support $mBatchSize, show the option.
312  // Used to be in addDefaultParams, but in order for that to
313  // work, subclasses would have to call this function in the constructor
314  // before they called parent::__construct which is just weird
315  // (and really wasn't done).
316  if ( $this->mBatchSize ) {
317  $this->addOption( 'batch-size', 'Run this many operations ' .
318  'per batch, default: ' . $this->mBatchSize, false, true );
319  if ( isset( $this->mParams['batch-size'] ) ) {
320  // This seems a little ugly...
321  $this->mDependantParameters['batch-size'] = $this->mParams['batch-size'];
322  }
323  }
324  }
325 
330  public function getName() {
331  return $this->mSelf;
332  }
333 
340  protected function getStdin( $len = null ) {
341  if ( $len == Maintenance::STDIN_ALL ) {
342  return file_get_contents( 'php://stdin' );
343  }
344  $f = fopen( 'php://stdin', 'rt' );
345  if ( !$len ) {
346  return $f;
347  }
348  $input = fgets( $f, $len );
349  fclose( $f );
350 
351  return rtrim( $input );
352  }
353 
357  public function isQuiet() {
358  return $this->mQuiet;
359  }
360 
367  protected function output( $out, $channel = null ) {
368  if ( $this->mQuiet ) {
369  return;
370  }
371  if ( $channel === null ) {
372  $this->cleanupChanneled();
373  print $out;
374  } else {
375  $out = preg_replace( '/\n\z/', '', $out );
376  $this->outputChanneled( $out, $channel );
377  }
378  }
379 
386  protected function error( $err, $die = 0 ) {
387  $this->outputChanneled( false );
388  if ( PHP_SAPI == 'cli' ) {
389  fwrite( STDERR, $err . "\n" );
390  } else {
391  print $err;
392  }
393  $die = intval( $die );
394  if ( $die > 0 ) {
395  die( $die );
396  }
397  }
398 
399  private $atLineStart = true;
400  private $lastChannel = null;
401 
405  public function cleanupChanneled() {
406  if ( !$this->atLineStart ) {
407  print "\n";
408  $this->atLineStart = true;
409  }
410  }
411 
420  public function outputChanneled( $msg, $channel = null ) {
421  if ( $msg === false ) {
422  $this->cleanupChanneled();
423 
424  return;
425  }
426 
427  // End the current line if necessary
428  if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
429  print "\n";
430  }
431 
432  print $msg;
433 
434  $this->atLineStart = false;
435  if ( $channel === null ) {
436  // For unchanneled messages, output trailing newline immediately
437  print "\n";
438  $this->atLineStart = true;
439  }
440  $this->lastChannel = $channel;
441  }
442 
453  public function getDbType() {
454  return Maintenance::DB_STD;
455  }
456 
460  protected function addDefaultParams() {
461 
462  # Generic (non script dependant) options:
463 
464  $this->addOption( 'help', 'Display this help message', false, false, 'h' );
465  $this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' );
466  $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true );
467  $this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
468  $this->addOption( 'globals', 'Output globals at the end of processing for debugging' );
469  $this->addOption(
470  'memory-limit',
471  'Set a specific memory limit for the script, '
472  . '"max" for no limit or "default" to avoid changing it'
473  );
474  $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
475  "http://en.wikipedia.org. This is sometimes necessary because " .
476  "server name detection may fail in command line scripts.", false, true );
477  $this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true );
478 
479  # Save generic options to display them separately in help
480  $this->mGenericParameters = $this->mParams;
481 
482  # Script dependant options:
483 
484  // If we support a DB, show the options
485  if ( $this->getDbType() > 0 ) {
486  $this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
487  $this->addOption( 'dbpass', 'The password to use for this script', false, true );
488  }
489 
490  # Save additional script dependant options to display
491  #  them separately in help
492  $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters );
493  }
494 
499  public function getConfig() {
500  if ( $this->config === null ) {
501  $this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
502  }
503 
504  return $this->config;
505  }
506 
511  public function setConfig( Config $config ) {
512  $this->config = $config;
513  }
514 
524  protected function requireExtension( $name ) {
525  $this->requiredExtensions[] = $name;
526  }
527 
533  public function checkRequiredExtensions() {
534  $registry = ExtensionRegistry::getInstance();
535  $missing = [];
536  foreach ( $this->requiredExtensions as $name ) {
537  if ( !$registry->isLoaded( $name ) ) {
538  $missing[] = $name;
539  }
540  }
541 
542  if ( $missing ) {
543  $joined = implode( ', ', $missing );
544  $msg = "The following extensions are required to be installed "
545  . "for this script to run: $joined. Please enable them and then try again.";
546  $this->error( $msg, 1 );
547  }
548 
549  }
550 
558  public function runChild( $maintClass, $classFile = null ) {
559  // Make sure the class is loaded first
560  if ( !class_exists( $maintClass ) ) {
561  if ( $classFile ) {
562  require_once $classFile;
563  }
564  if ( !class_exists( $maintClass ) ) {
565  $this->error( "Cannot spawn child: $maintClass" );
566  }
567  }
568 
572  $child = new $maintClass();
573  $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
574  if ( !is_null( $this->mDb ) ) {
575  $child->setDB( $this->mDb );
576  }
577 
578  return $child;
579  }
580 
584  public function setup() {
586 
587  # Abort if called from a web server
588  if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
589  $this->error( 'This script must be run from the command line', true );
590  }
591 
592  if ( $IP === null ) {
593  $this->error( "\$IP not set, aborting!\n" .
594  '(Did you forget to call parent::__construct() in your maintenance script?)', 1 );
595  }
596 
597  # Make sure we can handle script parameters
598  if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
599  $this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
600  }
601 
602  // Send PHP warnings and errors to stderr instead of stdout.
603  // This aids in diagnosing problems, while keeping messages
604  // out of redirected output.
605  if ( ini_get( 'display_errors' ) ) {
606  ini_set( 'display_errors', 'stderr' );
607  }
608 
609  $this->loadParamsAndArgs();
610  $this->maybeHelp();
611 
612  # Set the memory limit
613  # Note we need to set it again later in cache LocalSettings changed it
614  $this->adjustMemoryLimit();
615 
616  # Set max execution time to 0 (no limit). PHP.net says that
617  # "When running PHP from the command line the default setting is 0."
618  # But sometimes this doesn't seem to be the case.
619  ini_set( 'max_execution_time', 0 );
620 
621  $wgRequestTime = microtime( true );
622 
623  # Define us as being in MediaWiki
624  define( 'MEDIAWIKI', true );
625 
626  $wgCommandLineMode = true;
627 
628  # Turn off output buffering if it's on
629  while ( ob_get_level() > 0 ) {
630  ob_end_flush();
631  }
632 
633  $this->validateParamsAndArgs();
634  }
635 
645  public function memoryLimit() {
646  $limit = $this->getOption( 'memory-limit', 'max' );
647  $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
648  return $limit;
649  }
650 
654  protected function adjustMemoryLimit() {
655  $limit = $this->memoryLimit();
656  if ( $limit == 'max' ) {
657  $limit = -1; // no memory limit
658  }
659  if ( $limit != 'default' ) {
660  ini_set( 'memory_limit', $limit );
661  }
662  }
663 
667  protected function activateProfiler() {
669 
670  $output = $this->getOption( 'profiler' );
671  if ( !$output ) {
672  return;
673  }
674 
675  if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) {
676  $class = $wgProfiler['class'];
677  $profiler = new $class(
678  [ 'sampling' => 1, 'output' => [ $output ] ]
679  + $wgProfiler
680  + [ 'threshold' => $wgProfileLimit ]
681  );
682  $profiler->setTemplated( true );
683  Profiler::replaceStubInstance( $profiler );
684  }
685 
686  $trxProfiler = Profiler::instance()->getTransactionProfiler();
687  $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
688  $trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
689  }
690 
694  public function clearParamsAndArgs() {
695  $this->mOptions = [];
696  $this->mArgs = [];
697  $this->mInputLoaded = false;
698  }
699 
707  public function loadWithArgv( $argv ) {
708  $options = [];
709  $args = [];
710  $this->orderedOptions = [];
711 
712  # Parse arguments
713  for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
714  if ( $arg == '--' ) {
715  # End of options, remainder should be considered arguments
716  $arg = next( $argv );
717  while ( $arg !== false ) {
718  $args[] = $arg;
719  $arg = next( $argv );
720  }
721  break;
722  } elseif ( substr( $arg, 0, 2 ) == '--' ) {
723  # Long options
724  $option = substr( $arg, 2 );
725  if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
726  $param = next( $argv );
727  if ( $param === false ) {
728  $this->error( "\nERROR: $option parameter needs a value after it\n" );
729  $this->maybeHelp( true );
730  }
731 
732  $this->setParam( $options, $option, $param );
733  } else {
734  $bits = explode( '=', $option, 2 );
735  if ( count( $bits ) > 1 ) {
736  $option = $bits[0];
737  $param = $bits[1];
738  } else {
739  $param = 1;
740  }
741 
742  $this->setParam( $options, $option, $param );
743  }
744  } elseif ( $arg == '-' ) {
745  # Lonely "-", often used to indicate stdin or stdout.
746  $args[] = $arg;
747  } elseif ( substr( $arg, 0, 1 ) == '-' ) {
748  # Short options
749  $argLength = strlen( $arg );
750  for ( $p = 1; $p < $argLength; $p++ ) {
751  $option = $arg[$p];
752  if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
753  $option = $this->mShortParamsMap[$option];
754  }
755 
756  if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
757  $param = next( $argv );
758  if ( $param === false ) {
759  $this->error( "\nERROR: $option parameter needs a value after it\n" );
760  $this->maybeHelp( true );
761  }
762  $this->setParam( $options, $option, $param );
763  } else {
764  $this->setParam( $options, $option, 1 );
765  }
766  }
767  } else {
768  $args[] = $arg;
769  }
770  }
771 
772  $this->mOptions = $options;
773  $this->mArgs = $args;
774  $this->loadSpecialVars();
775  $this->mInputLoaded = true;
776  }
777 
790  private function setParam( &$options, $option, $value ) {
791  $this->orderedOptions[] = [ $option, $value ];
792 
793  if ( isset( $this->mParams[$option] ) ) {
794  $multi = $this->mParams[$option]['multiOccurrence'];
795  } else {
796  $multi = false;
797  }
798  $exists = array_key_exists( $option, $options );
799  if ( $multi && $exists ) {
800  $options[$option][] = $value;
801  } elseif ( $multi ) {
802  $options[$option] = [ $value ];
803  } elseif ( !$exists ) {
804  $options[$option] = $value;
805  } else {
806  $this->error( "\nERROR: $option parameter given twice\n" );
807  $this->maybeHelp( true );
808  }
809  }
810 
820  public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
821  # If we were given opts or args, set those and return early
822  if ( $self ) {
823  $this->mSelf = $self;
824  $this->mInputLoaded = true;
825  }
826  if ( $opts ) {
827  $this->mOptions = $opts;
828  $this->mInputLoaded = true;
829  }
830  if ( $args ) {
831  $this->mArgs = $args;
832  $this->mInputLoaded = true;
833  }
834 
835  # If we've already loaded input (either by user values or from $argv)
836  # skip on loading it again. The array_shift() will corrupt values if
837  # it's run again and again
838  if ( $this->mInputLoaded ) {
839  $this->loadSpecialVars();
840 
841  return;
842  }
843 
844  global $argv;
845  $this->mSelf = $argv[0];
846  $this->loadWithArgv( array_slice( $argv, 1 ) );
847  }
848 
852  protected function validateParamsAndArgs() {
853  $die = false;
854  # Check to make sure we've got all the required options
855  foreach ( $this->mParams as $opt => $info ) {
856  if ( $info['require'] && !$this->hasOption( $opt ) ) {
857  $this->error( "Param $opt required!" );
858  $die = true;
859  }
860  }
861  # Check arg list too
862  foreach ( $this->mArgList as $k => $info ) {
863  if ( $info['require'] && !$this->hasArg( $k ) ) {
864  $this->error( 'Argument <' . $info['name'] . '> required!' );
865  $die = true;
866  }
867  }
868 
869  if ( $die ) {
870  $this->maybeHelp( true );
871  }
872  }
873 
877  protected function loadSpecialVars() {
878  if ( $this->hasOption( 'dbuser' ) ) {
879  $this->mDbUser = $this->getOption( 'dbuser' );
880  }
881  if ( $this->hasOption( 'dbpass' ) ) {
882  $this->mDbPass = $this->getOption( 'dbpass' );
883  }
884  if ( $this->hasOption( 'quiet' ) ) {
885  $this->mQuiet = true;
886  }
887  if ( $this->hasOption( 'batch-size' ) ) {
888  $this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
889  }
890  }
891 
896  protected function maybeHelp( $force = false ) {
897  if ( !$force && !$this->hasOption( 'help' ) ) {
898  return;
899  }
900 
901  $screenWidth = 80; // TODO: Calculate this!
902  $tab = " ";
903  $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
904 
905  ksort( $this->mParams );
906  $this->mQuiet = false;
907 
908  // Description ...
909  if ( $this->mDescription ) {
910  $this->output( "\n" . $this->mDescription . "\n" );
911  }
912  $output = "\nUsage: php " . basename( $this->mSelf );
913 
914  // ... append parameters ...
915  if ( $this->mParams ) {
916  $output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
917  }
918 
919  // ... and append arguments.
920  if ( $this->mArgList ) {
921  $output .= ' ';
922  foreach ( $this->mArgList as $k => $arg ) {
923  if ( $arg['require'] ) {
924  $output .= '<' . $arg['name'] . '>';
925  } else {
926  $output .= '[' . $arg['name'] . ']';
927  }
928  if ( $k < count( $this->mArgList ) - 1 ) {
929  $output .= ' ';
930  }
931  }
932  }
933  $this->output( "$output\n\n" );
934 
935  # TODO abstract some repetitive code below
936 
937  // Generic parameters
938  $this->output( "Generic maintenance parameters:\n" );
939  foreach ( $this->mGenericParameters as $par => $info ) {
940  if ( $info['shortName'] !== false ) {
941  $par .= " (-{$info['shortName']})";
942  }
943  $this->output(
944  wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
945  "\n$tab$tab" ) . "\n"
946  );
947  }
948  $this->output( "\n" );
949 
950  $scriptDependantParams = $this->mDependantParameters;
951  if ( count( $scriptDependantParams ) > 0 ) {
952  $this->output( "Script dependant parameters:\n" );
953  // Parameters description
954  foreach ( $scriptDependantParams as $par => $info ) {
955  if ( $info['shortName'] !== false ) {
956  $par .= " (-{$info['shortName']})";
957  }
958  $this->output(
959  wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
960  "\n$tab$tab" ) . "\n"
961  );
962  }
963  $this->output( "\n" );
964  }
965 
966  // Script specific parameters not defined on construction by
967  // Maintenance::addDefaultParams()
968  $scriptSpecificParams = array_diff_key(
969  # all script parameters:
970  $this->mParams,
971  # remove the Maintenance default parameters:
972  $this->mGenericParameters,
973  $this->mDependantParameters
974  );
975  if ( count( $scriptSpecificParams ) > 0 ) {
976  $this->output( "Script specific parameters:\n" );
977  // Parameters description
978  foreach ( $scriptSpecificParams as $par => $info ) {
979  if ( $info['shortName'] !== false ) {
980  $par .= " (-{$info['shortName']})";
981  }
982  $this->output(
983  wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
984  "\n$tab$tab" ) . "\n"
985  );
986  }
987  $this->output( "\n" );
988  }
989 
990  // Print arguments
991  if ( count( $this->mArgList ) > 0 ) {
992  $this->output( "Arguments:\n" );
993  // Arguments description
994  foreach ( $this->mArgList as $info ) {
995  $openChar = $info['require'] ? '<' : '[';
996  $closeChar = $info['require'] ? '>' : ']';
997  $this->output(
998  wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
999  $info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1000  );
1001  }
1002  $this->output( "\n" );
1003  }
1004 
1005  die( 1 );
1006  }
1007 
1011  public function finalSetup() {
1015 
1016  # Turn off output buffering again, it might have been turned on in the settings files
1017  if ( ob_get_level() ) {
1018  ob_end_flush();
1019  }
1020  # Same with these
1021  $wgCommandLineMode = true;
1022 
1023  # Override $wgServer
1024  if ( $this->hasOption( 'server' ) ) {
1025  $wgServer = $this->getOption( 'server', $wgServer );
1026  }
1027 
1028  # If these were passed, use them
1029  if ( $this->mDbUser ) {
1030  $wgDBadminuser = $this->mDbUser;
1031  }
1032  if ( $this->mDbPass ) {
1033  $wgDBadminpassword = $this->mDbPass;
1034  }
1035 
1036  if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1037  $wgDBuser = $wgDBadminuser;
1038  $wgDBpassword = $wgDBadminpassword;
1039 
1040  if ( $wgDBservers ) {
1044  foreach ( $wgDBservers as $i => $server ) {
1045  $wgDBservers[$i]['user'] = $wgDBuser;
1046  $wgDBservers[$i]['password'] = $wgDBpassword;
1047  }
1048  }
1049  if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1050  $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1051  $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1052  }
1054  }
1055 
1056  // Per-script profiling; useful for debugging
1057  $this->activateProfiler();
1058 
1059  $this->afterFinalSetup();
1060 
1061  $wgShowSQLErrors = true;
1062 
1063  MediaWiki\suppressWarnings();
1064  set_time_limit( 0 );
1065  MediaWiki\restoreWarnings();
1066 
1067  $this->adjustMemoryLimit();
1068  }
1069 
1073  protected function afterFinalSetup() {
1074  if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1075  call_user_func( MW_CMDLINE_CALLBACK );
1076  }
1077  }
1078 
1083  public function globals() {
1084  if ( $this->hasOption( 'globals' ) ) {
1085  print_r( $GLOBALS );
1086  }
1087  }
1088 
1093  public function loadSettings() {
1095 
1096  if ( isset( $this->mOptions['conf'] ) ) {
1097  $settingsFile = $this->mOptions['conf'];
1098  } elseif ( defined( "MW_CONFIG_FILE" ) ) {
1099  $settingsFile = MW_CONFIG_FILE;
1100  } else {
1101  $settingsFile = "$IP/LocalSettings.php";
1102  }
1103  if ( isset( $this->mOptions['wiki'] ) ) {
1104  $bits = explode( '-', $this->mOptions['wiki'] );
1105  if ( count( $bits ) == 1 ) {
1106  $bits[] = '';
1107  }
1108  define( 'MW_DB', $bits[0] );
1109  define( 'MW_PREFIX', $bits[1] );
1110  }
1111 
1112  if ( !is_readable( $settingsFile ) ) {
1113  $this->error( "A copy of your installation's LocalSettings.php\n" .
1114  "must exist and be readable in the source directory.\n" .
1115  "Use --conf to specify it.", true );
1116  }
1117  $wgCommandLineMode = true;
1118 
1119  return $settingsFile;
1120  }
1121 
1127  public function purgeRedundantText( $delete = true ) {
1128  # Data should come off the master, wrapped in a transaction
1129  $dbw = $this->getDB( DB_MASTER );
1130  $this->beginTransaction( $dbw, __METHOD__ );
1131 
1132  # Get "active" text records from the revisions table
1133  $this->output( 'Searching for active text records in revisions table...' );
1134  $res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1135  foreach ( $res as $row ) {
1136  $cur[] = $row->rev_text_id;
1137  }
1138  $this->output( "done.\n" );
1139 
1140  # Get "active" text records from the archive table
1141  $this->output( 'Searching for active text records in archive table...' );
1142  $res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1143  foreach ( $res as $row ) {
1144  # old pre-MW 1.5 records can have null ar_text_id's.
1145  if ( $row->ar_text_id !== null ) {
1146  $cur[] = $row->ar_text_id;
1147  }
1148  }
1149  $this->output( "done.\n" );
1150 
1151  # Get the IDs of all text records not in these sets
1152  $this->output( 'Searching for inactive text records...' );
1153  $cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1154  $res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1155  $old = [];
1156  foreach ( $res as $row ) {
1157  $old[] = $row->old_id;
1158  }
1159  $this->output( "done.\n" );
1160 
1161  # Inform the user of what we're going to do
1162  $count = count( $old );
1163  $this->output( "$count inactive items found.\n" );
1164 
1165  # Delete as appropriate
1166  if ( $delete && $count ) {
1167  $this->output( 'Deleting...' );
1168  $dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1169  $this->output( "done.\n" );
1170  }
1171 
1172  # Done
1173  $this->commitTransaction( $dbw, __METHOD__ );
1174  }
1175 
1180  protected function getDir() {
1181  return __DIR__;
1182  }
1183 
1194  protected function getDB( $db, $groups = [], $wiki = false ) {
1195  if ( is_null( $this->mDb ) ) {
1196  return wfGetDB( $db, $groups, $wiki );
1197  } else {
1198  return $this->mDb;
1199  }
1200  }
1201 
1207  public function setDB( IDatabase $db ) {
1208  $this->mDb = $db;
1209  }
1210 
1221  protected function beginTransaction( IDatabase $dbw, $fname ) {
1222  $dbw->begin( $fname );
1223  }
1224 
1236  protected function commitTransaction( IDatabase $dbw, $fname ) {
1237  $dbw->commit( $fname );
1238 
1239  $ok = wfWaitForSlaves( $this->lastSlaveWait, false, '*', 30 );
1240  $this->lastSlaveWait = microtime( true );
1241 
1242  return $ok;
1243  }
1244 
1255  protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1256  $dbw->rollback( $fname );
1257  }
1258 
1263  private function lockSearchindex( $db ) {
1264  $write = [ 'searchindex' ];
1265  $read = [
1266  'page',
1267  'revision',
1268  'text',
1269  'interwiki',
1270  'l10n_cache',
1271  'user',
1272  'page_restrictions'
1273  ];
1274  $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1275  }
1276 
1281  private function unlockSearchindex( $db ) {
1282  $db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1283  }
1284 
1290  private function relockSearchindex( $db ) {
1291  $this->unlockSearchindex( $db );
1292  $this->lockSearchindex( $db );
1293  }
1294 
1302  public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1303  $lockTime = time();
1304 
1305  # Lock searchindex
1306  if ( $maxLockTime ) {
1307  $this->output( " --- Waiting for lock ---" );
1308  $this->lockSearchindex( $dbw );
1309  $lockTime = time();
1310  $this->output( "\n" );
1311  }
1312 
1313  # Loop through the results and do a search update
1314  foreach ( $results as $row ) {
1315  # Allow reads to be processed
1316  if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1317  $this->output( " --- Relocking ---" );
1318  $this->relockSearchindex( $dbw );
1319  $lockTime = time();
1320  $this->output( "\n" );
1321  }
1322  call_user_func( $callback, $dbw, $row );
1323  }
1324 
1325  # Unlock searchindex
1326  if ( $maxLockTime ) {
1327  $this->output( " --- Unlocking --" );
1328  $this->unlockSearchindex( $dbw );
1329  $this->output( "\n" );
1330  }
1331  }
1332 
1339  public function updateSearchIndexForPage( $dbw, $pageId ) {
1340  // Get current revision
1341  $rev = Revision::loadFromPageId( $dbw, $pageId );
1342  $title = null;
1343  if ( $rev ) {
1344  $titleObj = $rev->getTitle();
1345  $title = $titleObj->getPrefixedDBkey();
1346  $this->output( "$title..." );
1347  # Update searchindex
1348  $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() );
1349  $u->doUpdate();
1350  $this->output( "\n" );
1351  }
1352 
1353  return $title;
1354  }
1355 
1364  public static function posix_isatty( $fd ) {
1365  if ( !function_exists( 'posix_isatty' ) ) {
1366  return !$fd;
1367  } else {
1368  return posix_isatty( $fd );
1369  }
1370  }
1371 
1377  public static function readconsole( $prompt = '> ' ) {
1378  static $isatty = null;
1379  if ( is_null( $isatty ) ) {
1380  $isatty = self::posix_isatty( 0 /*STDIN*/ );
1381  }
1382 
1383  if ( $isatty && function_exists( 'readline' ) ) {
1384  $resp = readline( $prompt );
1385  if ( $resp === null ) {
1386  // Workaround for https://github.com/facebook/hhvm/issues/4776
1387  return false;
1388  } else {
1389  return $resp;
1390  }
1391  } else {
1392  if ( $isatty ) {
1393  $st = self::readlineEmulation( $prompt );
1394  } else {
1395  if ( feof( STDIN ) ) {
1396  $st = false;
1397  } else {
1398  $st = fgets( STDIN, 1024 );
1399  }
1400  }
1401  if ( $st === false ) {
1402  return false;
1403  }
1404  $resp = trim( $st );
1405 
1406  return $resp;
1407  }
1408  }
1409 
1415  private static function readlineEmulation( $prompt ) {
1416  $bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1417  if ( !wfIsWindows() && $bash ) {
1418  $retval = false;
1419  $encPrompt = wfEscapeShellArg( $prompt );
1420  $command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1421  $encCommand = wfEscapeShellArg( $command );
1422  $line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1423 
1424  if ( $retval == 0 ) {
1425  return $line;
1426  } elseif ( $retval == 127 ) {
1427  // Couldn't execute bash even though we thought we saw it.
1428  // Shell probably spit out an error message, sorry :(
1429  // Fall through to fgets()...
1430  } else {
1431  // EOF/ctrl+D
1432  return false;
1433  }
1434  }
1435 
1436  // Fallback... we'll have no editing controls, EWWW
1437  if ( feof( STDIN ) ) {
1438  return false;
1439  }
1440  print $prompt;
1441 
1442  return fgets( STDIN, 1024 );
1443  }
1444 }
1445 
1450  protected $mSelf = "FakeMaintenanceScript";
1451 
1452  public function execute() {
1453  return;
1454  }
1455 }
1456 
1461 abstract class LoggedUpdateMaintenance extends Maintenance {
1462  public function __construct() {
1463  parent::__construct();
1464  $this->addOption( 'force', 'Run the update even if it was completed already' );
1465  $this->setBatchSize( 200 );
1466  }
1467 
1468  public function execute() {
1469  $db = $this->getDB( DB_MASTER );
1470  $key = $this->getUpdateKey();
1471 
1472  if ( !$this->hasOption( 'force' )
1473  && $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1474  ) {
1475  $this->output( "..." . $this->updateSkippedMessage() . "\n" );
1476 
1477  return true;
1478  }
1479 
1480  if ( !$this->doDBUpdates() ) {
1481  return false;
1482  }
1483 
1484  if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1485  return true;
1486  } else {
1487  $this->output( $this->updatelogFailedMessage() . "\n" );
1488 
1489  return false;
1490  }
1491  }
1492 
1497  protected function updateSkippedMessage() {
1498  $key = $this->getUpdateKey();
1499 
1500  return "Update '{$key}' already logged as completed.";
1501  }
1502 
1507  protected function updatelogFailedMessage() {
1508  $key = $this->getUpdateKey();
1509 
1510  return "Unable to log update '{$key}' as completed.";
1511  }
1512 
1518  abstract protected function doDBUpdates();
1519 
1524  abstract protected function getUpdateKey();
1525 }
#define the
table suitable for use with IDatabase::select()
unlockSearchindex($db)
Unlock the tables.
loadParamsAndArgs($self=null, $opts=null, $args=null)
Process command line arguments $mOptions becomes an array with keys set to the option names $mArgs be...
commitTransaction(IDatabase $dbw, $fname)
Commit the transcation on a DB handle and wait for slaves to catch up.
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
const STDIN_ALL
Definition: Maintenance.php:61
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:776
const DB_NONE
Constants for DB access type.
Definition: Maintenance.php:56
wfWaitForSlaves($ifWritesSince=null, $wiki=false, $cluster=false, $timeout=null)
Waits for the slaves to catch up to the master position.
const RUN_MAINTENANCE_IF_MAIN
Definition: Maintenance.php:34
checkRequiredExtensions()
Verify that the required extensions are installed.
float $lastSlaveWait
UNIX timestamp.
setParam(&$options, $option, $value)
Helper function used solely by loadParamsAndArgs to prevent code duplication.
$IP
Definition: WebStart.php:58
$wgDBadminpassword
Separate password for maintenance tasks.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
addArg($arg, $description, $required=true)
Add some args that are needed.
getDir()
Get the maintenance directory.
array $orderedOptions
Used to read the options in the order they were passed.
$command
Definition: cdb.php:65
$wgShowSQLErrors
Whether to show "we're sorry, but there has been a database error" pages.
static instance()
Singleton.
Definition: Profiler.php:60
$wgDBpassword
Database user's password.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: maintenance.txt:39
begin($fname=__METHOD__)
Begin a transaction.
getName()
Get the script's name.
doUpdate()
Perform actual update for the entry.
$wgProfiler
Definition: WebStart.php:73
addDefaultParams()
Add the default parameters to the scripts.
finalSetup()
Handle some last-minute setup here.
rollbackTransaction(IDatabase $dbw, $fname)
Rollback the transcation on a DB handle.
lockSearchindex($db)
Lock the search index.
afterFinalSetup()
Execute a callback function at the end of initialisation.
$value
setConfig(Config $config)
getDB($db, $groups=[], $wiki=false)
Returns a database to be used by current maintenance script.
hasOption($name)
Checks to see if a particular param exists.
$wgDBuser
Database username.
static locateExecutableInDefaultPaths($names, $versionInfo=false)
Same as locateExecutable(), but checks in getPossibleBinPaths() by default.
Definition: Installer.php:1238
loadSpecialVars()
Handle the special variables that are global to all scripts.
wfShellExec($cmd, &$retval=null, $environ=[], $limits=[], $options=[])
Execute a shell command, with time and memory limits mirrored from the PHP configuration if supported...
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
wfIsWindows()
Check if the operating system is Windows.
static posix_isatty($fd)
Wrapper for posix_isatty() We default as considering stdin a tty (for nice readline methods) but trea...
array $requiredExtensions
getStdin($len=null)
Return input from stdin.
$maintClass
Definition: Maintenance.php:37
if($line===false) $args
Definition: cdb.php:64
cleanupChanneled()
Clean up channeled output.
static replaceStubInstance(Profiler $profiler)
Replace the current profiler with $profiler if no non-stub profiler is set.
Definition: Profiler.php:95
Database independant search index updater.
updatelogFailedMessage()
Message to show that the update log was unable to log the completion of this update.
Fake maintenance wrapper, mostly used for the web installer/updater.
activateProfiler()
Activate the profiler (assuming $wgProfiler is set)
addOption($name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
global $wgCommandLineMode
Definition: Setup.php:511
$wgTrxProfilerLimits
Performance expectations for DB usage.
Interface for configuration instances.
Definition: Config.php:28
$GLOBALS['IP']
deleteOption($name)
Remove an option.
doDBUpdates()
Do the actual work.
updateSearchIndex($maxLockTime, $callback, $dbw, $results)
Perform a search index update with locking.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1020
relockSearchindex($db)
Unlock and lock again Since the lock is low-priority, queued reads will be able to complete...
$self
$res
Definition: database.txt:21
const DB_ADMIN
Definition: Maintenance.php:58
setup()
Do some sanity checking and basic setup.
setDB(IDatabase $db)
Sets database object to be returned by getDB().
clearParamsAndArgs()
Clear all params and arguments.
hasArg($argId=0)
Does a given argument exist?
static destroyInstance()
Shut down, close connections and destroy the cached instance.
Definition: LBFactory.php:128
$wgDBservers
Database load balancer This is a two-dimensional array, an array of server info structures Fields are...
globals()
Potentially debug globals.
Allows to change the fields on the form that will be generated are created Can be used to omit specific feeds from being outputted You must not use this hook to add use OutputPage::addFeedLink() instead.&$feedLinks conditions will AND in the final query as a Content object as a Content object $title
Definition: hooks.txt:312
adjustMemoryLimit()
Adjusts PHP's memory limit to better suit our needs, if needed.
getUpdateKey()
Get the update key name to go in the update log table.
requireExtension($name)
Indicate that the specified extension must be loaded before the script can run.
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1601
resource $fileHandle
Used when creating separate schema files.
addDescription($text)
Set the description text.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getOption($name, $default=null)
Get an option, or return the default.
IDatabase $mDb
Used by getDB() / setDB()
static shouldExecute()
Should we execute the maintenance script, or just allow it to be included as a standalone class...
output($out, $channel=null)
Throw some output to the user.
purgeRedundantText($delete=true)
Support function for cleaning up redundant text records.
const DB_STD
Definition: Maintenance.php:57
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object & $output
Definition: hooks.txt:1020
static getDefaultInstance()
commit($fname=__METHOD__, $flush= '')
Commits a transaction previously started using begin().
loadWithArgv($argv)
Load params and arguments from a given array of command-line arguments.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
maybeHelp($force=false)
Maybe show the help.
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined...
Definition: Setup.php:36
Class for scripts that perform database maintenance and want to log the update in updatelog so we can...
$wgDBadminuser
Separate username for maintenance tasks.
static readconsole($prompt= '> ')
Prompt the console for input.
static readlineEmulation($prompt)
Emulate readline()
memoryLimit()
Normally we disable the memory_limit when running admin scripts.
$wgProfileLimit
Only record profiling info for pages that took longer than this.
int $mBatchSize
Batch size.
Definition: Maintenance.php:97
$line
Definition: cdb.php:59
error($err, $die=0)
Throw an error to the user.
static loadFromPageId($db, $pageid, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition: Revision.php:245
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition: hooks.txt:1020
wfEntryPointCheck($entryPoint)
Check php version and that external dependencies are installed, and display an informative error if e...
updateSearchIndexForPage($dbw, $pageId)
Update the searchindex table for a given pageid.
updateSkippedMessage()
Message to show that the update was done already and was just skipped.
MediaWiki Logger LoggerFactory implements a PSR[0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method.MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances.The"Spi"in MediaWiki\Logger\Spi stands for"service provider interface".An SPI is an API intended to be implemented or extended by a third party.This software design pattern is intended to enable framework extension and replaceable components.It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki.The service provider interface allows the backend logging library to be implemented in multiple ways.The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime.This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance.Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
$count
wfEscapeShellArg()
Windows-compatible version of escapeshellarg() Windows doesn't recognise single-quotes in the shell...
$wgServer
URL of the server.
const DB_MASTER
Definition: Defines.php:47
getArg($argId=0, $default=null)
Get an argument.
$wgLBFactoryConf
Load balancer factory configuration To set up a multi-master wiki farm, set the class here to somethi...
loadSettings()
Generic setup for most installs.
rollback($fname=__METHOD__, $flush= '')
Rollback a transaction previously started using begin().
setBatchSize($s=0)
Set the batch size.
float $wgRequestTime
Request start time as fractional seconds since epoch.
Definition: WebStart.php:43
validateParamsAndArgs()
Run some validation checks on the params, etc.
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
Definition: hooks.txt:242
Config $config
Accessible via getConfig()
runChild($maintClass, $classFile=null)
Run a child maintenance script.
outputChanneled($msg, $channel=null)
Message outputter with channeled message support.
getDbType()
Does the script need different DB access? By default, we give Maintenance scripts normal rights to th...
public function execute()
Definition: maintenance.txt:45
Basic database interface for live and lazy-loaded DB handles.
Definition: IDatabase.php:35
__construct()
Default constructor.
beginTransaction(IDatabase $dbw, $fname)
Begin a transcation on a DB.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:310