Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

maintenance/Maintenance.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 * @ingroup Maintenance
20
 * @defgroup Maintenance Maintenance
21
 */
22
23
// 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
28
/**
29
 * @defgroup MaintenanceArchive Maintenance archives
30
 * @ingroup Maintenance
31
 */
32
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
39
use MediaWiki\Logger\LoggerFactory;
40
use MediaWiki\MediaWikiServices;
41
42
/**
43
 * Abstract maintenance class for quickly writing and churning out
44
 * maintenance scripts with minimal effort. All that _must_ be defined
45
 * is the execute() method. See docs/maintenance.txt for more info
46
 * and a quick demo of how to use it.
47
 *
48
 * @author Chad Horohoe <[email protected]>
49
 * @since 1.16
50
 * @ingroup Maintenance
51
 */
52
abstract class Maintenance {
53
	/**
54
	 * Constants for DB access type
55
	 * @see Maintenance::getDbType()
56
	 */
57
	const DB_NONE = 0;
58
	const DB_STD = 1;
59
	const DB_ADMIN = 2;
60
61
	// Const for getStdin()
62
	const STDIN_ALL = 'all';
63
64
	// This is the desired params
65
	protected $mParams = [];
66
67
	// Array of mapping short parameters to long ones
68
	protected $mShortParamsMap = [];
69
70
	// Array of desired args
71
	protected $mArgList = [];
72
73
	// This is the list of options that were actually passed
74
	protected $mOptions = [];
75
76
	// This is the list of arguments that were actually passed
77
	protected $mArgs = [];
78
79
	// Name of the script currently running
80
	protected $mSelf;
81
82
	// Special vars for params that are always used
83
	protected $mQuiet = false;
84
	protected $mDbUser, $mDbPass;
85
86
	// A description of the script, children should change this via addDescription()
87
	protected $mDescription = '';
88
89
	// Have we already loaded our user input?
90
	protected $mInputLoaded = false;
91
92
	/**
93
	 * Batch size. If a script supports this, they should set
94
	 * a default with setBatchSize()
95
	 *
96
	 * @var int
97
	 */
98
	protected $mBatchSize = null;
99
100
	// Generic options added by addDefaultParams()
101
	private $mGenericParameters = [];
102
	// Generic options which might or not be supported by the script
103
	private $mDependantParameters = [];
104
105
	/**
106
	 * Used by getDB() / setDB()
107
	 * @var Database
108
	 */
109
	private $mDb = null;
110
111
	/** @var float UNIX timestamp */
112
	private $lastReplicationWait = 0.0;
113
114
	/**
115
	 * Used when creating separate schema files.
116
	 * @var resource
117
	 */
118
	public $fileHandle;
119
120
	/**
121
	 * Accessible via getConfig()
122
	 *
123
	 * @var Config
124
	 */
125
	private $config;
126
127
	/**
128
	 * @see Maintenance::requireExtension
129
	 * @var array
130
	 */
131
	private $requiredExtensions = [];
132
133
	/**
134
	 * Used to read the options in the order they were passed.
135
	 * Useful for option chaining (Ex. dumpBackup.php). It will
136
	 * be an empty array if the options are passed in through
137
	 * loadParamsAndArgs( $self, $opts, $args ).
138
	 *
139
	 * This is an array of arrays where
140
	 * 0 => the option and 1 => parameter value.
141
	 *
142
	 * @var array
143
	 */
144
	public $orderedOptions = [];
145
146
	/**
147
	 * Default constructor. Children should call this *first* if implementing
148
	 * their own constructors
149
	 */
150
	public function __construct() {
151
		// Setup $IP, using MW_INSTALL_PATH if it exists
152
		global $IP;
153
		$IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
154
			? getenv( 'MW_INSTALL_PATH' )
155
			: realpath( __DIR__ . '/..' );
156
157
		$this->addDefaultParams();
158
		register_shutdown_function( [ $this, 'outputChanneled' ], false );
159
	}
160
161
	/**
162
	 * Should we execute the maintenance script, or just allow it to be included
163
	 * as a standalone class? It checks that the call stack only includes this
164
	 * function and "requires" (meaning was called from the file scope)
165
	 *
166
	 * @return bool
167
	 */
168
	public static function shouldExecute() {
169
		global $wgCommandLineMode;
170
171
		if ( !function_exists( 'debug_backtrace' ) ) {
172
			// If someone has a better idea...
173
			return $wgCommandLineMode;
174
		}
175
176
		$bt = debug_backtrace();
177
		$count = count( $bt );
178
		if ( $count < 2 ) {
179
			return false; // sanity
180
		}
181
		if ( $bt[0]['class'] !== 'Maintenance' || $bt[0]['function'] !== 'shouldExecute' ) {
182
			return false; // last call should be to this function
183
		}
184
		$includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ];
185
		for ( $i = 1; $i < $count; $i++ ) {
186
			if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) {
187
				return false; // previous calls should all be "requires"
188
			}
189
		}
190
191
		return true;
192
	}
193
194
	/**
195
	 * Do the actual work. All child classes will need to implement this
196
	 */
197
	abstract public function execute();
198
199
	/**
200
	 * Add a parameter to the script. Will be displayed on --help
201
	 * with the associated description
202
	 *
203
	 * @param string $name The name of the param (help, version, etc)
204
	 * @param string $description The description of the param to show on --help
205
	 * @param bool $required Is the param required?
206
	 * @param bool $withArg Is an argument required with this option?
207
	 * @param string $shortName Character to use as short name
208
	 * @param bool $multiOccurrence Can this option be passed multiple times?
209
	 */
210
	protected function addOption( $name, $description, $required = false,
211
		$withArg = false, $shortName = false, $multiOccurrence = false
212
	) {
213
		$this->mParams[$name] = [
214
			'desc' => $description,
215
			'require' => $required,
216
			'withArg' => $withArg,
217
			'shortName' => $shortName,
218
			'multiOccurrence' => $multiOccurrence
219
		];
220
221
		if ( $shortName !== false ) {
222
			$this->mShortParamsMap[$shortName] = $name;
223
		}
224
	}
225
226
	/**
227
	 * Checks to see if a particular param exists.
228
	 * @param string $name The name of the param
229
	 * @return bool
230
	 */
231
	protected function hasOption( $name ) {
232
		return isset( $this->mOptions[$name] );
233
	}
234
235
	/**
236
	 * Get an option, or return the default.
237
	 *
238
	 * If the option was added to support multiple occurrences,
239
	 * this will return an array.
240
	 *
241
	 * @param string $name The name of the param
242
	 * @param mixed $default Anything you want, default null
243
	 * @return mixed
244
	 */
245
	protected function getOption( $name, $default = null ) {
246
		if ( $this->hasOption( $name ) ) {
247
			return $this->mOptions[$name];
248
		} else {
249
			// Set it so we don't have to provide the default again
250
			$this->mOptions[$name] = $default;
251
252
			return $this->mOptions[$name];
253
		}
254
	}
255
256
	/**
257
	 * Add some args that are needed
258
	 * @param string $arg Name of the arg, like 'start'
259
	 * @param string $description Short description of the arg
260
	 * @param bool $required Is this required?
261
	 */
262
	protected function addArg( $arg, $description, $required = true ) {
263
		$this->mArgList[] = [
264
			'name' => $arg,
265
			'desc' => $description,
266
			'require' => $required
267
		];
268
	}
269
270
	/**
271
	 * Remove an option.  Useful for removing options that won't be used in your script.
272
	 * @param string $name The option to remove.
273
	 */
274
	protected function deleteOption( $name ) {
275
		unset( $this->mParams[$name] );
276
	}
277
278
	/**
279
	 * Set the description text.
280
	 * @param string $text The text of the description
281
	 */
282
	protected function addDescription( $text ) {
283
		$this->mDescription = $text;
284
	}
285
286
	/**
287
	 * Does a given argument exist?
288
	 * @param int $argId The integer value (from zero) for the arg
289
	 * @return bool
290
	 */
291
	protected function hasArg( $argId = 0 ) {
292
		return isset( $this->mArgs[$argId] );
293
	}
294
295
	/**
296
	 * Get an argument.
297
	 * @param int $argId The integer value (from zero) for the arg
298
	 * @param mixed $default The default if it doesn't exist
299
	 * @return mixed
300
	 */
301
	protected function getArg( $argId = 0, $default = null ) {
302
		return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default;
303
	}
304
305
	/**
306
	 * Set the batch size.
307
	 * @param int $s The number of operations to do in a batch
308
	 */
309
	protected function setBatchSize( $s = 0 ) {
310
		$this->mBatchSize = $s;
311
312
		// If we support $mBatchSize, show the option.
313
		// Used to be in addDefaultParams, but in order for that to
314
		// work, subclasses would have to call this function in the constructor
315
		// before they called parent::__construct which is just weird
316
		// (and really wasn't done).
317
		if ( $this->mBatchSize ) {
318
			$this->addOption( 'batch-size', 'Run this many operations ' .
319
				'per batch, default: ' . $this->mBatchSize, false, true );
320
			if ( isset( $this->mParams['batch-size'] ) ) {
321
				// This seems a little ugly...
322
				$this->mDependantParameters['batch-size'] = $this->mParams['batch-size'];
323
			}
324
		}
325
	}
326
327
	/**
328
	 * Get the script's name
329
	 * @return string
330
	 */
331
	public function getName() {
332
		return $this->mSelf;
333
	}
334
335
	/**
336
	 * Return input from stdin.
337
	 * @param int $len The number of bytes to read. If null, just return the handle.
338
	 *   Maintenance::STDIN_ALL returns the full length
339
	 * @return mixed
340
	 */
341
	protected function getStdin( $len = null ) {
342
		if ( $len == Maintenance::STDIN_ALL ) {
343
			return file_get_contents( 'php://stdin' );
344
		}
345
		$f = fopen( 'php://stdin', 'rt' );
346
		if ( !$len ) {
347
			return $f;
348
		}
349
		$input = fgets( $f, $len );
350
		fclose( $f );
351
352
		return rtrim( $input );
353
	}
354
355
	/**
356
	 * @return bool
357
	 */
358
	public function isQuiet() {
359
		return $this->mQuiet;
360
	}
361
362
	/**
363
	 * Throw some output to the user. Scripts can call this with no fears,
364
	 * as we handle all --quiet stuff here
365
	 * @param string $out The text to show to the user
366
	 * @param mixed $channel Unique identifier for the channel. See function outputChanneled.
367
	 */
368
	protected function output( $out, $channel = null ) {
369
		if ( $this->mQuiet ) {
370
			return;
371
		}
372
		if ( $channel === null ) {
373
			$this->cleanupChanneled();
374
			print $out;
375
		} else {
376
			$out = preg_replace( '/\n\z/', '', $out );
377
			$this->outputChanneled( $out, $channel );
378
		}
379
	}
380
381
	/**
382
	 * Throw an error to the user. Doesn't respect --quiet, so don't use
383
	 * this for non-error output
384
	 * @param string $err The error to display
385
	 * @param int $die If > 0, go ahead and die out using this int as the code
386
	 */
387
	protected function error( $err, $die = 0 ) {
388
		$this->outputChanneled( false );
389
		if ( PHP_SAPI == 'cli' ) {
390
			fwrite( STDERR, $err . "\n" );
391
		} else {
392
			print $err;
393
		}
394
		$die = intval( $die );
395
		if ( $die > 0 ) {
396
			die( $die );
397
		}
398
	}
399
400
	private $atLineStart = true;
401
	private $lastChannel = null;
402
403
	/**
404
	 * Clean up channeled output.  Output a newline if necessary.
405
	 */
406
	public function cleanupChanneled() {
407
		if ( !$this->atLineStart ) {
408
			print "\n";
409
			$this->atLineStart = true;
410
		}
411
	}
412
413
	/**
414
	 * Message outputter with channeled message support. Messages on the
415
	 * same channel are concatenated, but any intervening messages in another
416
	 * channel start a new line.
417
	 * @param string $msg The message without trailing newline
418
	 * @param string $channel Channel identifier or null for no
419
	 *     channel. Channel comparison uses ===.
420
	 */
421
	public function outputChanneled( $msg, $channel = null ) {
422
		if ( $msg === false ) {
423
			$this->cleanupChanneled();
424
425
			return;
426
		}
427
428
		// End the current line if necessary
429
		if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
430
			print "\n";
431
		}
432
433
		print $msg;
434
435
		$this->atLineStart = false;
436
		if ( $channel === null ) {
437
			// For unchanneled messages, output trailing newline immediately
438
			print "\n";
439
			$this->atLineStart = true;
440
		}
441
		$this->lastChannel = $channel;
442
	}
443
444
	/**
445
	 * Does the script need different DB access? By default, we give Maintenance
446
	 * scripts normal rights to the DB. Sometimes, a script needs admin rights
447
	 * access for a reason and sometimes they want no access. Subclasses should
448
	 * override and return one of the following values, as needed:
449
	 *    Maintenance::DB_NONE  -  For no DB access at all
450
	 *    Maintenance::DB_STD   -  For normal DB access, default
451
	 *    Maintenance::DB_ADMIN -  For admin DB access
452
	 * @return int
453
	 */
454
	public function getDbType() {
455
		return Maintenance::DB_STD;
456
	}
457
458
	/**
459
	 * Add the default parameters to the scripts
460
	 */
461
	protected function addDefaultParams() {
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
495
	/**
496
	 * @since 1.24
497
	 * @return Config
498
	 */
499 View Code Duplication
	public function getConfig() {
500
		if ( $this->config === null ) {
501
			$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
502
		}
503
504
		return $this->config;
505
	}
506
507
	/**
508
	 * @since 1.24
509
	 * @param Config $config
510
	 */
511
	public function setConfig( Config $config ) {
512
		$this->config = $config;
513
	}
514
515
	/**
516
	 * Indicate that the specified extension must be
517
	 * loaded before the script can run.
518
	 *
519
	 * This *must* be called in the constructor.
520
	 *
521
	 * @since 1.28
522
	 * @param string $name
523
	 */
524
	protected function requireExtension( $name ) {
525
		$this->requiredExtensions[] = $name;
526
	}
527
528
	/**
529
	 * Verify that the required extensions are installed
530
	 *
531
	 * @since 1.28
532
	 */
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
	/**
551
	 * Set triggers like when to try to run deferred updates
552
	 * @since 1.28
553
	 */
554
	public function setAgentAndTriggers() {
555
		if ( function_exists( 'posix_getpwuid' ) ) {
556
			$agent = posix_getpwuid( posix_geteuid() )['name'];
557
		} else {
558
			$agent = 'sysadmin';
559
		}
560
		$agent .= '@' . wfHostname();
561
562
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
563
		// Add a comment for easy SHOW PROCESSLIST interpretation
564
		$lbFactory->setAgentName(
565
			mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
566
		);
567
		self::setLBFactoryTriggers( $lbFactory );
568
	}
569
570
	/**
571
	 * @param LBFactory $LBFactory
572
	 * @since 1.28
573
	 */
574
	public static function setLBFactoryTriggers( LBFactory $LBFactory ) {
575
		// Hook into period lag checks which often happen in long-running scripts
576
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
577
		$lbFactory->setWaitForReplicationListener(
578
			__METHOD__,
579
			function () {
580
				global $wgCommandLineMode;
581
				// Check config in case of JobRunner and unit tests
582
				if ( $wgCommandLineMode ) {
583
					DeferredUpdates::tryOpportunisticExecute( 'run' );
584
				}
585
			}
586
		);
587
		// Check for other windows to run them. A script may read or do a few writes
588
		// to the master but mostly be writing to something else, like a file store.
589
		$lbFactory->getMainLB()->setTransactionListener(
590
			__METHOD__,
591
			function ( $trigger ) {
592
				global $wgCommandLineMode;
593
				// Check config in case of JobRunner and unit tests
594
				if ( $wgCommandLineMode && $trigger === IDatabase::TRIGGER_COMMIT ) {
595
					DeferredUpdates::tryOpportunisticExecute( 'run' );
596
				}
597
			}
598
		);
599
	}
600
601
	/**
602
	 * Run a child maintenance script. Pass all of the current arguments
603
	 * to it.
604
	 * @param string $maintClass A name of a child maintenance class
605
	 * @param string $classFile Full path of where the child is
606
	 * @return Maintenance
607
	 */
608
	public function runChild( $maintClass, $classFile = null ) {
609
		// Make sure the class is loaded first
610
		if ( !class_exists( $maintClass ) ) {
611
			if ( $classFile ) {
612
				require_once $classFile;
613
			}
614
			if ( !class_exists( $maintClass ) ) {
615
				$this->error( "Cannot spawn child: $maintClass" );
616
			}
617
		}
618
619
		/**
620
		 * @var $child Maintenance
621
		 */
622
		$child = new $maintClass();
623
		$child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
624
		if ( !is_null( $this->mDb ) ) {
625
			$child->setDB( $this->mDb );
626
		}
627
628
		return $child;
629
	}
630
631
	/**
632
	 * Do some sanity checking and basic setup
633
	 */
634
	public function setup() {
635
		global $IP, $wgCommandLineMode, $wgRequestTime;
636
637
		# Abort if called from a web server
638
		if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
639
			$this->error( 'This script must be run from the command line', true );
640
		}
641
642
		if ( $IP === null ) {
643
			$this->error( "\$IP not set, aborting!\n" .
644
				'(Did you forget to call parent::__construct() in your maintenance script?)', 1 );
645
		}
646
647
		# Make sure we can handle script parameters
648
		if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
649
			$this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
650
		}
651
652
		// Send PHP warnings and errors to stderr instead of stdout.
653
		// This aids in diagnosing problems, while keeping messages
654
		// out of redirected output.
655
		if ( ini_get( 'display_errors' ) ) {
656
			ini_set( 'display_errors', 'stderr' );
657
		}
658
659
		$this->loadParamsAndArgs();
660
		$this->maybeHelp();
661
662
		# Set the memory limit
663
		# Note we need to set it again later in cache LocalSettings changed it
664
		$this->adjustMemoryLimit();
665
666
		# Set max execution time to 0 (no limit). PHP.net says that
667
		# "When running PHP from the command line the default setting is 0."
668
		# But sometimes this doesn't seem to be the case.
669
		ini_set( 'max_execution_time', 0 );
670
671
		$wgRequestTime = microtime( true );
672
673
		# Define us as being in MediaWiki
674
		define( 'MEDIAWIKI', true );
675
676
		$wgCommandLineMode = true;
677
678
		# Turn off output buffering if it's on
679
		while ( ob_get_level() > 0 ) {
680
			ob_end_flush();
681
		}
682
683
		$this->validateParamsAndArgs();
684
	}
685
686
	/**
687
	 * Normally we disable the memory_limit when running admin scripts.
688
	 * Some scripts may wish to actually set a limit, however, to avoid
689
	 * blowing up unexpectedly. We also support a --memory-limit option,
690
	 * to allow sysadmins to explicitly set one if they'd prefer to override
691
	 * defaults (or for people using Suhosin which yells at you for trying
692
	 * to disable the limits)
693
	 * @return string
694
	 */
695
	public function memoryLimit() {
696
		$limit = $this->getOption( 'memory-limit', 'max' );
697
		$limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
698
		return $limit;
699
	}
700
701
	/**
702
	 * Adjusts PHP's memory limit to better suit our needs, if needed.
703
	 */
704
	protected function adjustMemoryLimit() {
705
		$limit = $this->memoryLimit();
706
		if ( $limit == 'max' ) {
707
			$limit = -1; // no memory limit
708
		}
709
		if ( $limit != 'default' ) {
710
			ini_set( 'memory_limit', $limit );
711
		}
712
	}
713
714
	/**
715
	 * Activate the profiler (assuming $wgProfiler is set)
716
	 */
717
	protected function activateProfiler() {
718
		global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits;
719
720
		$output = $this->getOption( 'profiler' );
721
		if ( !$output ) {
722
			return;
723
		}
724
725
		if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) {
726
			$class = $wgProfiler['class'];
727
			/** @var Profiler $profiler */
728
			$profiler = new $class(
729
				[ 'sampling' => 1, 'output' => [ $output ] ]
730
					+ $wgProfiler
731
					+ [ 'threshold' => $wgProfileLimit ]
732
			);
733
			$profiler->setTemplated( true );
734
			Profiler::replaceStubInstance( $profiler );
735
		}
736
737
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
738
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
739
		$trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
740
	}
741
742
	/**
743
	 * Clear all params and arguments.
744
	 */
745
	public function clearParamsAndArgs() {
746
		$this->mOptions = [];
747
		$this->mArgs = [];
748
		$this->mInputLoaded = false;
749
	}
750
751
	/**
752
	 * Load params and arguments from a given array
753
	 * of command-line arguments
754
	 *
755
	 * @since 1.27
756
	 * @param array $argv
757
	 */
758
	public function loadWithArgv( $argv ) {
759
		$options = [];
760
		$args = [];
761
		$this->orderedOptions = [];
762
763
		# Parse arguments
764
		for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
765
			if ( $arg == '--' ) {
766
				# End of options, remainder should be considered arguments
767
				$arg = next( $argv );
768
				while ( $arg !== false ) {
769
					$args[] = $arg;
770
					$arg = next( $argv );
771
				}
772
				break;
773
			} elseif ( substr( $arg, 0, 2 ) == '--' ) {
774
				# Long options
775
				$option = substr( $arg, 2 );
776
				if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
777
					$param = next( $argv );
778
					if ( $param === false ) {
779
						$this->error( "\nERROR: $option parameter needs a value after it\n" );
780
						$this->maybeHelp( true );
781
					}
782
783
					$this->setParam( $options, $option, $param );
784
				} else {
785
					$bits = explode( '=', $option, 2 );
786
					if ( count( $bits ) > 1 ) {
787
						$option = $bits[0];
788
						$param = $bits[1];
789
					} else {
790
						$param = 1;
791
					}
792
793
					$this->setParam( $options, $option, $param );
794
				}
795
			} elseif ( $arg == '-' ) {
796
				# Lonely "-", often used to indicate stdin or stdout.
797
				$args[] = $arg;
798
			} elseif ( substr( $arg, 0, 1 ) == '-' ) {
799
				# Short options
800
				$argLength = strlen( $arg );
801
				for ( $p = 1; $p < $argLength; $p++ ) {
802
					$option = $arg[$p];
803
					if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
804
						$option = $this->mShortParamsMap[$option];
805
					}
806
807
					if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
808
						$param = next( $argv );
809
						if ( $param === false ) {
810
							$this->error( "\nERROR: $option parameter needs a value after it\n" );
811
							$this->maybeHelp( true );
812
						}
813
						$this->setParam( $options, $option, $param );
814
					} else {
815
						$this->setParam( $options, $option, 1 );
816
					}
817
				}
818
			} else {
819
				$args[] = $arg;
820
			}
821
		}
822
823
		$this->mOptions = $options;
824
		$this->mArgs = $args;
825
		$this->loadSpecialVars();
826
		$this->mInputLoaded = true;
827
	}
828
829
	/**
830
	 * Helper function used solely by loadParamsAndArgs
831
	 * to prevent code duplication
832
	 *
833
	 * This sets the param in the options array based on
834
	 * whether or not it can be specified multiple times.
835
	 *
836
	 * @since 1.27
837
	 * @param array $options
838
	 * @param string $option
839
	 * @param mixed $value
840
	 */
841
	private function setParam( &$options, $option, $value ) {
842
		$this->orderedOptions[] = [ $option, $value ];
843
844
		if ( isset( $this->mParams[$option] ) ) {
845
			$multi = $this->mParams[$option]['multiOccurrence'];
846
		} else {
847
			$multi = false;
848
		}
849
		$exists = array_key_exists( $option, $options );
850
		if ( $multi && $exists ) {
851
			$options[$option][] = $value;
852
		} elseif ( $multi ) {
853
			$options[$option] = [ $value ];
854
		} elseif ( !$exists ) {
855
			$options[$option] = $value;
856
		} else {
857
			$this->error( "\nERROR: $option parameter given twice\n" );
858
			$this->maybeHelp( true );
859
		}
860
	}
861
862
	/**
863
	 * Process command line arguments
864
	 * $mOptions becomes an array with keys set to the option names
865
	 * $mArgs becomes a zero-based array containing the non-option arguments
866
	 *
867
	 * @param string $self The name of the script, if any
868
	 * @param array $opts An array of options, in form of key=>value
869
	 * @param array $args An array of command line arguments
870
	 */
871
	public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
872
		# If we were given opts or args, set those and return early
873
		if ( $self ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $self of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
874
			$this->mSelf = $self;
875
			$this->mInputLoaded = true;
876
		}
877
		if ( $opts ) {
878
			$this->mOptions = $opts;
879
			$this->mInputLoaded = true;
880
		}
881
		if ( $args ) {
882
			$this->mArgs = $args;
883
			$this->mInputLoaded = true;
884
		}
885
886
		# If we've already loaded input (either by user values or from $argv)
887
		# skip on loading it again. The array_shift() will corrupt values if
888
		# it's run again and again
889
		if ( $this->mInputLoaded ) {
890
			$this->loadSpecialVars();
891
892
			return;
893
		}
894
895
		global $argv;
896
		$this->mSelf = $argv[0];
897
		$this->loadWithArgv( array_slice( $argv, 1 ) );
898
	}
899
900
	/**
901
	 * Run some validation checks on the params, etc
902
	 */
903
	protected function validateParamsAndArgs() {
904
		$die = false;
905
		# Check to make sure we've got all the required options
906
		foreach ( $this->mParams as $opt => $info ) {
907
			if ( $info['require'] && !$this->hasOption( $opt ) ) {
908
				$this->error( "Param $opt required!" );
909
				$die = true;
910
			}
911
		}
912
		# Check arg list too
913
		foreach ( $this->mArgList as $k => $info ) {
914
			if ( $info['require'] && !$this->hasArg( $k ) ) {
915
				$this->error( 'Argument <' . $info['name'] . '> required!' );
916
				$die = true;
917
			}
918
		}
919
920
		if ( $die ) {
921
			$this->maybeHelp( true );
922
		}
923
	}
924
925
	/**
926
	 * Handle the special variables that are global to all scripts
927
	 */
928
	protected function loadSpecialVars() {
929
		if ( $this->hasOption( 'dbuser' ) ) {
930
			$this->mDbUser = $this->getOption( 'dbuser' );
931
		}
932
		if ( $this->hasOption( 'dbpass' ) ) {
933
			$this->mDbPass = $this->getOption( 'dbpass' );
934
		}
935
		if ( $this->hasOption( 'quiet' ) ) {
936
			$this->mQuiet = true;
937
		}
938
		if ( $this->hasOption( 'batch-size' ) ) {
939
			$this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
940
		}
941
	}
942
943
	/**
944
	 * Maybe show the help.
945
	 * @param bool $force Whether to force the help to show, default false
946
	 */
947
	protected function maybeHelp( $force = false ) {
948
		if ( !$force && !$this->hasOption( 'help' ) ) {
949
			return;
950
		}
951
952
		$screenWidth = 80; // TODO: Calculate this!
953
		$tab = "    ";
954
		$descWidth = $screenWidth - ( 2 * strlen( $tab ) );
955
956
		ksort( $this->mParams );
957
		$this->mQuiet = false;
958
959
		// Description ...
960
		if ( $this->mDescription ) {
961
			$this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
962
		}
963
		$output = "\nUsage: php " . basename( $this->mSelf );
964
965
		// ... append parameters ...
966
		if ( $this->mParams ) {
967
			$output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
968
		}
969
970
		// ... and append arguments.
971
		if ( $this->mArgList ) {
972
			$output .= ' ';
973
			foreach ( $this->mArgList as $k => $arg ) {
974
				if ( $arg['require'] ) {
975
					$output .= '<' . $arg['name'] . '>';
976
				} else {
977
					$output .= '[' . $arg['name'] . ']';
978
				}
979
				if ( $k < count( $this->mArgList ) - 1 ) {
980
					$output .= ' ';
981
				}
982
			}
983
		}
984
		$this->output( "$output\n\n" );
985
986
		# TODO abstract some repetitive code below
987
988
		// Generic parameters
989
		$this->output( "Generic maintenance parameters:\n" );
990
		foreach ( $this->mGenericParameters as $par => $info ) {
991
			if ( $info['shortName'] !== false ) {
992
				$par .= " (-{$info['shortName']})";
993
			}
994
			$this->output(
995
				wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
996
					"\n$tab$tab" ) . "\n"
997
			);
998
		}
999
		$this->output( "\n" );
1000
1001
		$scriptDependantParams = $this->mDependantParameters;
1002 View Code Duplication
		if ( count( $scriptDependantParams ) > 0 ) {
1003
			$this->output( "Script dependant parameters:\n" );
1004
			// Parameters description
1005
			foreach ( $scriptDependantParams as $par => $info ) {
1006
				if ( $info['shortName'] !== false ) {
1007
					$par .= " (-{$info['shortName']})";
1008
				}
1009
				$this->output(
1010
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1011
						"\n$tab$tab" ) . "\n"
1012
				);
1013
			}
1014
			$this->output( "\n" );
1015
		}
1016
1017
		// Script specific parameters not defined on construction by
1018
		// Maintenance::addDefaultParams()
1019
		$scriptSpecificParams = array_diff_key(
1020
			# all script parameters:
1021
			$this->mParams,
1022
			# remove the Maintenance default parameters:
1023
			$this->mGenericParameters,
1024
			$this->mDependantParameters
1025
		);
1026 View Code Duplication
		if ( count( $scriptSpecificParams ) > 0 ) {
1027
			$this->output( "Script specific parameters:\n" );
1028
			// Parameters description
1029
			foreach ( $scriptSpecificParams as $par => $info ) {
1030
				if ( $info['shortName'] !== false ) {
1031
					$par .= " (-{$info['shortName']})";
1032
				}
1033
				$this->output(
1034
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1035
						"\n$tab$tab" ) . "\n"
1036
				);
1037
			}
1038
			$this->output( "\n" );
1039
		}
1040
1041
		// Print arguments
1042
		if ( count( $this->mArgList ) > 0 ) {
1043
			$this->output( "Arguments:\n" );
1044
			// Arguments description
1045
			foreach ( $this->mArgList as $info ) {
1046
				$openChar = $info['require'] ? '<' : '[';
1047
				$closeChar = $info['require'] ? '>' : ']';
1048
				$this->output(
1049
					wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
1050
						$info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1051
				);
1052
			}
1053
			$this->output( "\n" );
1054
		}
1055
1056
		die( 1 );
1057
	}
1058
1059
	/**
1060
	 * Handle some last-minute setup here.
1061
	 */
1062
	public function finalSetup() {
1063
		global $wgCommandLineMode, $wgShowSQLErrors, $wgServer;
1064
		global $wgDBadminuser, $wgDBadminpassword;
1065
		global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
1066
1067
		# Turn off output buffering again, it might have been turned on in the settings files
1068
		if ( ob_get_level() ) {
1069
			ob_end_flush();
1070
		}
1071
		# Same with these
1072
		$wgCommandLineMode = true;
1073
1074
		# Override $wgServer
1075
		if ( $this->hasOption( 'server' ) ) {
1076
			$wgServer = $this->getOption( 'server', $wgServer );
1077
		}
1078
1079
		# If these were passed, use them
1080
		if ( $this->mDbUser ) {
1081
			$wgDBadminuser = $this->mDbUser;
1082
		}
1083
		if ( $this->mDbPass ) {
1084
			$wgDBadminpassword = $this->mDbPass;
1085
		}
1086
1087
		if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1088
			$wgDBuser = $wgDBadminuser;
1089
			$wgDBpassword = $wgDBadminpassword;
1090
1091
			if ( $wgDBservers ) {
1092
				/**
1093
				 * @var $wgDBservers array
1094
				 */
1095
				foreach ( $wgDBservers as $i => $server ) {
1096
					$wgDBservers[$i]['user'] = $wgDBuser;
1097
					$wgDBservers[$i]['password'] = $wgDBpassword;
1098
				}
1099
			}
1100
			if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1101
				$wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1102
				$wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1103
			}
1104
			MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy();
1105
		}
1106
1107
		// Per-script profiling; useful for debugging
1108
		$this->activateProfiler();
1109
1110
		$this->afterFinalSetup();
1111
1112
		$wgShowSQLErrors = true;
1113
1114
		MediaWiki\suppressWarnings();
1115
		set_time_limit( 0 );
1116
		MediaWiki\restoreWarnings();
1117
1118
		$this->adjustMemoryLimit();
1119
	}
1120
1121
	/**
1122
	 * Execute a callback function at the end of initialisation
1123
	 */
1124
	protected function afterFinalSetup() {
1125
		if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1126
			call_user_func( MW_CMDLINE_CALLBACK );
1127
		}
1128
	}
1129
1130
	/**
1131
	 * Potentially debug globals. Originally a feature only
1132
	 * for refreshLinks
1133
	 */
1134
	public function globals() {
1135
		if ( $this->hasOption( 'globals' ) ) {
1136
			print_r( $GLOBALS );
1137
		}
1138
	}
1139
1140
	/**
1141
	 * Generic setup for most installs. Returns the location of LocalSettings
1142
	 * @return string
1143
	 */
1144
	public function loadSettings() {
1145
		global $wgCommandLineMode, $IP;
1146
1147
		if ( isset( $this->mOptions['conf'] ) ) {
1148
			$settingsFile = $this->mOptions['conf'];
1149
		} elseif ( defined( "MW_CONFIG_FILE" ) ) {
1150
			$settingsFile = MW_CONFIG_FILE;
1151
		} else {
1152
			$settingsFile = "$IP/LocalSettings.php";
1153
		}
1154
		if ( isset( $this->mOptions['wiki'] ) ) {
1155
			$bits = explode( '-', $this->mOptions['wiki'] );
1156
			if ( count( $bits ) == 1 ) {
1157
				$bits[] = '';
1158
			}
1159
			define( 'MW_DB', $bits[0] );
1160
			define( 'MW_PREFIX', $bits[1] );
1161
		}
1162
1163
		if ( !is_readable( $settingsFile ) ) {
1164
			$this->error( "A copy of your installation's LocalSettings.php\n" .
1165
				"must exist and be readable in the source directory.\n" .
1166
				"Use --conf to specify it.", true );
1167
		}
1168
		$wgCommandLineMode = true;
1169
1170
		return $settingsFile;
1171
	}
1172
1173
	/**
1174
	 * Support function for cleaning up redundant text records
1175
	 * @param bool $delete Whether or not to actually delete the records
1176
	 * @author Rob Church <[email protected]>
1177
	 */
1178
	public function purgeRedundantText( $delete = true ) {
1179
		# Data should come off the master, wrapped in a transaction
1180
		$dbw = $this->getDB( DB_MASTER );
1181
		$this->beginTransaction( $dbw, __METHOD__ );
1182
1183
		# Get "active" text records from the revisions table
1184
		$cur = [];
1185
		$this->output( 'Searching for active text records in revisions table...' );
1186
		$res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1187
		foreach ( $res as $row ) {
1188
			$cur[] = $row->rev_text_id;
1189
		}
1190
		$this->output( "done.\n" );
1191
1192
		# Get "active" text records from the archive table
1193
		$this->output( 'Searching for active text records in archive table...' );
1194
		$res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1195
		foreach ( $res as $row ) {
1196
			# old pre-MW 1.5 records can have null ar_text_id's.
1197
			if ( $row->ar_text_id !== null ) {
1198
				$cur[] = $row->ar_text_id;
1199
			}
1200
		}
1201
		$this->output( "done.\n" );
1202
1203
		# Get the IDs of all text records not in these sets
1204
		$this->output( 'Searching for inactive text records...' );
1205
		$cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1206
		$res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1207
		$old = [];
1208
		foreach ( $res as $row ) {
1209
			$old[] = $row->old_id;
1210
		}
1211
		$this->output( "done.\n" );
1212
1213
		# Inform the user of what we're going to do
1214
		$count = count( $old );
1215
		$this->output( "$count inactive items found.\n" );
1216
1217
		# Delete as appropriate
1218
		if ( $delete && $count ) {
1219
			$this->output( 'Deleting...' );
1220
			$dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1221
			$this->output( "done.\n" );
1222
		}
1223
1224
		# Done
1225
		$this->commitTransaction( $dbw, __METHOD__ );
1226
	}
1227
1228
	/**
1229
	 * Get the maintenance directory.
1230
	 * @return string
1231
	 */
1232
	protected function getDir() {
1233
		return __DIR__;
1234
	}
1235
1236
	/**
1237
	 * Returns a database to be used by current maintenance script. It can be set by setDB().
1238
	 * If not set, wfGetDB() will be used.
1239
	 * This function has the same parameters as wfGetDB()
1240
	 *
1241
	 * @param integer $db DB index (DB_REPLICA/DB_MASTER)
1242
	 * @param array $groups; default: empty array
1243
	 * @param string|bool $wiki; default: current wiki
1244
	 * @return Database
1245
	 */
1246
	protected function getDB( $db, $groups = [], $wiki = false ) {
1247
		if ( is_null( $this->mDb ) ) {
1248
			return wfGetDB( $db, $groups, $wiki );
1249
		} else {
1250
			return $this->mDb;
1251
		}
1252
	}
1253
1254
	/**
1255
	 * Sets database object to be returned by getDB().
1256
	 *
1257
	 * @param IDatabase $db Database object to be used
1258
	 */
1259
	public function setDB( IDatabase $db ) {
1260
		$this->mDb = $db;
1261
	}
1262
1263
	/**
1264
	 * Begin a transcation on a DB
1265
	 *
1266
	 * This method makes it clear that begin() is called from a maintenance script,
1267
	 * which has outermost scope. This is safe, unlike $dbw->begin() called in other places.
1268
	 *
1269
	 * @param IDatabase $dbw
1270
	 * @param string $fname Caller name
1271
	 * @since 1.27
1272
	 */
1273
	protected function beginTransaction( IDatabase $dbw, $fname ) {
1274
		$dbw->begin( $fname );
1275
	}
1276
1277
	/**
1278
	 * Commit the transcation on a DB handle and wait for replica DBs to catch up
1279
	 *
1280
	 * This method makes it clear that commit() is called from a maintenance script,
1281
	 * which has outermost scope. This is safe, unlike $dbw->commit() called in other places.
1282
	 *
1283
	 * @param IDatabase $dbw
1284
	 * @param string $fname Caller name
1285
	 * @return bool Whether the replica DB wait succeeded
1286
	 * @since 1.27
1287
	 */
1288
	protected function commitTransaction( IDatabase $dbw, $fname ) {
1289
		$dbw->commit( $fname );
1290
		try {
1291
			$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1292
			$lbFactory->waitForReplication(
1293
				[ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1294
			);
1295
			$this->lastReplicationWait = microtime( true );
1296
1297
			return true;
1298
		} catch ( DBReplicationWaitError $e ) {
1299
			return false;
1300
		}
1301
	}
1302
1303
	/**
1304
	 * Rollback the transcation on a DB handle
1305
	 *
1306
	 * This method makes it clear that rollback() is called from a maintenance script,
1307
	 * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1308
	 *
1309
	 * @param IDatabase $dbw
1310
	 * @param string $fname Caller name
1311
	 * @since 1.27
1312
	 */
1313
	protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1314
		$dbw->rollback( $fname );
1315
	}
1316
1317
	/**
1318
	 * Lock the search index
1319
	 * @param Database &$db
1320
	 */
1321
	private function lockSearchindex( $db ) {
1322
		$write = [ 'searchindex' ];
1323
		$read = [
1324
			'page',
1325
			'revision',
1326
			'text',
1327
			'interwiki',
1328
			'l10n_cache',
1329
			'user',
1330
			'page_restrictions'
1331
		];
1332
		$db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1333
	}
1334
1335
	/**
1336
	 * Unlock the tables
1337
	 * @param Database &$db
1338
	 */
1339
	private function unlockSearchindex( $db ) {
1340
		$db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1341
	}
1342
1343
	/**
1344
	 * Unlock and lock again
1345
	 * Since the lock is low-priority, queued reads will be able to complete
1346
	 * @param Database &$db
1347
	 */
1348
	private function relockSearchindex( $db ) {
1349
		$this->unlockSearchindex( $db );
1350
		$this->lockSearchindex( $db );
1351
	}
1352
1353
	/**
1354
	 * Perform a search index update with locking
1355
	 * @param int $maxLockTime The maximum time to keep the search index locked.
1356
	 * @param string $callback The function that will update the function.
1357
	 * @param Database $dbw
1358
	 * @param array $results
1359
	 */
1360
	public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1361
		$lockTime = time();
1362
1363
		# Lock searchindex
1364
		if ( $maxLockTime ) {
1365
			$this->output( "   --- Waiting for lock ---" );
1366
			$this->lockSearchindex( $dbw );
1367
			$lockTime = time();
1368
			$this->output( "\n" );
1369
		}
1370
1371
		# Loop through the results and do a search update
1372
		foreach ( $results as $row ) {
1373
			# Allow reads to be processed
1374
			if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1375
				$this->output( "    --- Relocking ---" );
1376
				$this->relockSearchindex( $dbw );
1377
				$lockTime = time();
1378
				$this->output( "\n" );
1379
			}
1380
			call_user_func( $callback, $dbw, $row );
1381
		}
1382
1383
		# Unlock searchindex
1384
		if ( $maxLockTime ) {
1385
			$this->output( "    --- Unlocking --" );
1386
			$this->unlockSearchindex( $dbw );
1387
			$this->output( "\n" );
1388
		}
1389
	}
1390
1391
	/**
1392
	 * Update the searchindex table for a given pageid
1393
	 * @param Database $dbw A database write handle
1394
	 * @param int $pageId The page ID to update.
1395
	 * @return null|string
1396
	 */
1397
	public function updateSearchIndexForPage( $dbw, $pageId ) {
1398
		// Get current revision
1399
		$rev = Revision::loadFromPageId( $dbw, $pageId );
1400
		$title = null;
1401
		if ( $rev ) {
1402
			$titleObj = $rev->getTitle();
1403
			$title = $titleObj->getPrefixedDBkey();
1404
			$this->output( "$title..." );
1405
			# Update searchindex
1406
			$u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() );
1407
			$u->doUpdate();
1408
			$this->output( "\n" );
1409
		}
1410
1411
		return $title;
1412
	}
1413
1414
	/**
1415
	 * Wrapper for posix_isatty()
1416
	 * We default as considering stdin a tty (for nice readline methods)
1417
	 * but treating stout as not a tty to avoid color codes
1418
	 *
1419
	 * @param mixed $fd File descriptor
1420
	 * @return bool
1421
	 */
1422
	public static function posix_isatty( $fd ) {
1423
		if ( !function_exists( 'posix_isatty' ) ) {
1424
			return !$fd;
1425
		} else {
1426
			return posix_isatty( $fd );
1427
		}
1428
	}
1429
1430
	/**
1431
	 * Prompt the console for input
1432
	 * @param string $prompt What to begin the line with, like '> '
1433
	 * @return string Response
1434
	 */
1435
	public static function readconsole( $prompt = '> ' ) {
1436
		static $isatty = null;
1437
		if ( is_null( $isatty ) ) {
1438
			$isatty = self::posix_isatty( 0 /*STDIN*/ );
1439
		}
1440
1441
		if ( $isatty && function_exists( 'readline' ) ) {
1442
			$resp = readline( $prompt );
1443
			if ( $resp === null ) {
1444
				// Workaround for https://github.com/facebook/hhvm/issues/4776
1445
				return false;
1446
			} else {
1447
				return $resp;
1448
			}
1449
		} else {
1450
			if ( $isatty ) {
1451
				$st = self::readlineEmulation( $prompt );
1452
			} else {
1453
				if ( feof( STDIN ) ) {
1454
					$st = false;
1455
				} else {
1456
					$st = fgets( STDIN, 1024 );
1457
				}
1458
			}
1459
			if ( $st === false ) {
1460
				return false;
1461
			}
1462
			$resp = trim( $st );
1463
1464
			return $resp;
1465
		}
1466
	}
1467
1468
	/**
1469
	 * Emulate readline()
1470
	 * @param string $prompt What to begin the line with, like '> '
1471
	 * @return string
1472
	 */
1473
	private static function readlineEmulation( $prompt ) {
1474
		$bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1475
		if ( !wfIsWindows() && $bash ) {
1476
			$retval = false;
1477
			$encPrompt = wfEscapeShellArg( $prompt );
1478
			$command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1479
			$encCommand = wfEscapeShellArg( $command );
1480
			$line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1481
1482
			if ( $retval == 0 ) {
1483
				return $line;
1484
			} elseif ( $retval == 127 ) {
1485
				// Couldn't execute bash even though we thought we saw it.
1486
				// Shell probably spit out an error message, sorry :(
1487
				// Fall through to fgets()...
1488
			} else {
1489
				// EOF/ctrl+D
1490
				return false;
1491
			}
1492
		}
1493
1494
		// Fallback... we'll have no editing controls, EWWW
1495
		if ( feof( STDIN ) ) {
1496
			return false;
1497
		}
1498
		print $prompt;
1499
1500
		return fgets( STDIN, 1024 );
1501
	}
1502
1503
	/**
1504
	 * Get the terminal size as a two-element array where the first element
1505
	 * is the width (number of columns) and the second element is the height
1506
	 * (number of rows).
1507
	 *
1508
	 * @return array
1509
	 */
1510
	public static function getTermSize() {
1511
		$default = [ 80, 50 ];
1512
		if ( wfIsWindows() ) {
1513
			return $default;
1514
		}
1515
		// It's possible to get the screen size with VT-100 terminal escapes,
1516
		// but reading the responses is not possible without setting raw mode
1517
		// (unless you want to require the user to press enter), and that
1518
		// requires an ioctl(), which we can't do. So we have to shell out to
1519
		// something that can do the relevant syscalls. There are a few
1520
		// options. Linux and Mac OS X both have "stty size" which does the
1521
		// job directly.
1522
		$retval = false;
1523
		$size = wfShellExec( 'stty size', $retval );
1524
		if ( $retval !== 0 ) {
1525
			return $default;
1526
		}
1527
		if ( !preg_match( '/^(\d+) (\d+)$/', $size, $m ) ) {
1528
			return $default;
1529
		}
1530
		return [ intval( $m[2] ), intval( $m[1] ) ];
1531
	}
1532
1533
	/**
1534
	 * Call this to set up the autoloader to allow classes to be used from the
1535
	 * tests directory.
1536
	 */
1537
	public static function requireTestsAutoloader() {
1538
		require_once __DIR__ . '/../tests/common/TestsAutoLoader.php';
1539
	}
1540
}
1541
1542
/**
1543
 * Fake maintenance wrapper, mostly used for the web installer/updater
1544
 */
1545
class FakeMaintenance extends Maintenance {
1546
	protected $mSelf = "FakeMaintenanceScript";
1547
1548
	public function execute() {
1549
		return;
1550
	}
1551
}
1552
1553
/**
1554
 * Class for scripts that perform database maintenance and want to log the
1555
 * update in `updatelog` so we can later skip it
1556
 */
1557
abstract class LoggedUpdateMaintenance extends Maintenance {
1558
	public function __construct() {
1559
		parent::__construct();
1560
		$this->addOption( 'force', 'Run the update even if it was completed already' );
1561
		$this->setBatchSize( 200 );
1562
	}
1563
1564
	public function execute() {
1565
		$db = $this->getDB( DB_MASTER );
1566
		$key = $this->getUpdateKey();
1567
1568
		if ( !$this->hasOption( 'force' )
1569
			&& $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1570
		) {
1571
			$this->output( "..." . $this->updateSkippedMessage() . "\n" );
1572
1573
			return true;
1574
		}
1575
1576
		if ( !$this->doDBUpdates() ) {
1577
			return false;
1578
		}
1579
1580 View Code Duplication
		if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1581
			return true;
1582
		} else {
1583
			$this->output( $this->updatelogFailedMessage() . "\n" );
1584
1585
			return false;
1586
		}
1587
	}
1588
1589
	/**
1590
	 * Message to show that the update was done already and was just skipped
1591
	 * @return string
1592
	 */
1593
	protected function updateSkippedMessage() {
1594
		$key = $this->getUpdateKey();
1595
1596
		return "Update '{$key}' already logged as completed.";
1597
	}
1598
1599
	/**
1600
	 * Message to show that the update log was unable to log the completion of this update
1601
	 * @return string
1602
	 */
1603
	protected function updatelogFailedMessage() {
1604
		$key = $this->getUpdateKey();
1605
1606
		return "Unable to log update '{$key}' as completed.";
1607
	}
1608
1609
	/**
1610
	 * Do the actual work. All child classes will need to implement this.
1611
	 * Return true to log the update as done or false (usually on failure).
1612
	 * @return bool
1613
	 */
1614
	abstract protected function doDBUpdates();
1615
1616
	/**
1617
	 * Get the update key name to go in the update log table
1618
	 * @return string
1619
	 */
1620
	abstract protected function getUpdateKey();
1621
}
1622