Maintenance.php 40 KB

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