Completed
Branch master (19cd63)
by
unknown
40:04
created

Maintenance::getTermSize()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 0
dl 0
loc 22
rs 8.9197
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 34 and the first side effect is on line 25.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $len of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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 );
0 ignored issues
show
Coding Style Compatibility introduced by
The method error() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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
463
		# Generic (non script dependant) options:
464
465
		$this->addOption( 'help', 'Display this help message', false, false, 'h' );
466
		$this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' );
467
		$this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true );
468
		$this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
469
		$this->addOption( 'globals', 'Output globals at the end of processing for debugging' );
470
		$this->addOption(
471
			'memory-limit',
472
			'Set a specific memory limit for the script, '
473
				. '"max" for no limit or "default" to avoid changing it'
474
		);
475
		$this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
476
			"http://en.wikipedia.org. This is sometimes necessary because " .
477
			"server name detection may fail in command line scripts.", false, true );
478
		$this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true );
479
480
		# Save generic options to display them separately in help
481
		$this->mGenericParameters = $this->mParams;
482
483
		# Script dependant options:
484
485
		// If we support a DB, show the options
486
		if ( $this->getDbType() > 0 ) {
487
			$this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
488
			$this->addOption( 'dbpass', 'The password to use for this script', false, true );
489
		}
490
491
		# Save additional script dependant options to display
492
		#  them separately in help
493
		$this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters );
494
	}
495
496
	/**
497
	 * @since 1.24
498
	 * @return Config
499
	 */
500 View Code Duplication
	public function getConfig() {
501
		if ( $this->config === null ) {
502
			$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
503
		}
504
505
		return $this->config;
506
	}
507
508
	/**
509
	 * @since 1.24
510
	 * @param Config $config
511
	 */
512
	public function setConfig( Config $config ) {
513
		$this->config = $config;
514
	}
515
516
	/**
517
	 * Indicate that the specified extension must be
518
	 * loaded before the script can run.
519
	 *
520
	 * This *must* be called in the constructor.
521
	 *
522
	 * @since 1.28
523
	 * @param string $name
524
	 */
525
	protected function requireExtension( $name ) {
526
		$this->requiredExtensions[] = $name;
527
	}
528
529
	/**
530
	 * Verify that the required extensions are installed
531
	 *
532
	 * @since 1.28
533
	 */
534
	public function checkRequiredExtensions() {
535
		$registry = ExtensionRegistry::getInstance();
536
		$missing = [];
537
		foreach ( $this->requiredExtensions as $name ) {
538
			if ( !$registry->isLoaded( $name ) ) {
539
				$missing[] = $name;
540
			}
541
		}
542
543
		if ( $missing ) {
544
			$joined = implode( ', ', $missing );
545
			$msg = "The following extensions are required to be installed "
546
				. "for this script to run: $joined. Please enable them and then try again.";
547
			$this->error( $msg, 1 );
548
		}
549
550
	}
551
552
	/**
553
	 * Set triggers like when to try to run deferred updates
554
	 * @since 1.28
555
	 */
556
	public function setAgentAndTriggers() {
557
		if ( function_exists( 'posix_getpwuid' ) ) {
558
			$agent = posix_getpwuid( posix_geteuid() )['name'];
559
		} else {
560
			$agent = 'sysadmin';
561
		}
562
		$agent .= '@' . wfHostname();
563
564
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
565
		// Add a comment for easy SHOW PROCESSLIST interpretation
566
		$lbFactory->setAgentName(
567
			mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
568
		);
569
		self::setLBFactoryTriggers( $lbFactory );
570
	}
571
572
	/**
573
	 * @param LBFactory $LBFactory
574
	 * @since 1.28
575
	 */
576
	public static function setLBFactoryTriggers( LBFactory $LBFactory ) {
577
		// Hook into period lag checks which often happen in long-running scripts
578
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
579
		$lbFactory->setWaitForReplicationListener(
580
			__METHOD__,
581
			function () {
582
				global $wgCommandLineMode;
583
				// Check config in case of JobRunner and unit tests
584
				if ( $wgCommandLineMode ) {
585
					DeferredUpdates::tryOpportunisticExecute( 'run' );
586
				}
587
			}
588
		);
589
		// Check for other windows to run them. A script may read or do a few writes
590
		// to the master but mostly be writing to something else, like a file store.
591
		$lbFactory->getMainLB()->setTransactionListener(
592
			__METHOD__,
593
			function ( $trigger ) {
594
				global $wgCommandLineMode;
595
				// Check config in case of JobRunner and unit tests
596
				if ( $wgCommandLineMode && $trigger === IDatabase::TRIGGER_COMMIT ) {
597
					DeferredUpdates::tryOpportunisticExecute( 'run' );
598
				}
599
			}
600
		);
601
	}
602
603
	/**
604
	 * Run a child maintenance script. Pass all of the current arguments
605
	 * to it.
606
	 * @param string $maintClass A name of a child maintenance class
607
	 * @param string $classFile Full path of where the child is
608
	 * @return Maintenance
609
	 */
610
	public function runChild( $maintClass, $classFile = null ) {
611
		// Make sure the class is loaded first
612
		if ( !class_exists( $maintClass ) ) {
613
			if ( $classFile ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $classFile 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...
614
				require_once $classFile;
615
			}
616
			if ( !class_exists( $maintClass ) ) {
617
				$this->error( "Cannot spawn child: $maintClass" );
618
			}
619
		}
620
621
		/**
622
		 * @var $child Maintenance
623
		 */
624
		$child = new $maintClass();
625
		$child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
626
		if ( !is_null( $this->mDb ) ) {
627
			$child->setDB( $this->mDb );
628
		}
629
630
		return $child;
631
	}
632
633
	/**
634
	 * Do some sanity checking and basic setup
635
	 */
636
	public function setup() {
0 ignored issues
show
Coding Style introduced by
setup uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
637
		global $IP, $wgCommandLineMode, $wgRequestTime;
638
639
		# Abort if called from a web server
640
		if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
641
			$this->error( 'This script must be run from the command line', true );
642
		}
643
644
		if ( $IP === null ) {
645
			$this->error( "\$IP not set, aborting!\n" .
646
				'(Did you forget to call parent::__construct() in your maintenance script?)', 1 );
647
		}
648
649
		# Make sure we can handle script parameters
650
		if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
651
			$this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
652
		}
653
654
		// Send PHP warnings and errors to stderr instead of stdout.
655
		// This aids in diagnosing problems, while keeping messages
656
		// out of redirected output.
657
		if ( ini_get( 'display_errors' ) ) {
658
			ini_set( 'display_errors', 'stderr' );
659
		}
660
661
		$this->loadParamsAndArgs();
662
		$this->maybeHelp();
663
664
		# Set the memory limit
665
		# Note we need to set it again later in cache LocalSettings changed it
666
		$this->adjustMemoryLimit();
667
668
		# Set max execution time to 0 (no limit). PHP.net says that
669
		# "When running PHP from the command line the default setting is 0."
670
		# But sometimes this doesn't seem to be the case.
671
		ini_set( 'max_execution_time', 0 );
672
673
		$wgRequestTime = microtime( true );
674
675
		# Define us as being in MediaWiki
676
		define( 'MEDIAWIKI', true );
677
678
		$wgCommandLineMode = true;
679
680
		# Turn off output buffering if it's on
681
		while ( ob_get_level() > 0 ) {
682
			ob_end_flush();
683
		}
684
685
		$this->validateParamsAndArgs();
686
	}
687
688
	/**
689
	 * Normally we disable the memory_limit when running admin scripts.
690
	 * Some scripts may wish to actually set a limit, however, to avoid
691
	 * blowing up unexpectedly. We also support a --memory-limit option,
692
	 * to allow sysadmins to explicitly set one if they'd prefer to override
693
	 * defaults (or for people using Suhosin which yells at you for trying
694
	 * to disable the limits)
695
	 * @return string
696
	 */
697
	public function memoryLimit() {
698
		$limit = $this->getOption( 'memory-limit', 'max' );
699
		$limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
700
		return $limit;
701
	}
702
703
	/**
704
	 * Adjusts PHP's memory limit to better suit our needs, if needed.
705
	 */
706
	protected function adjustMemoryLimit() {
707
		$limit = $this->memoryLimit();
708
		if ( $limit == 'max' ) {
709
			$limit = -1; // no memory limit
710
		}
711
		if ( $limit != 'default' ) {
712
			ini_set( 'memory_limit', $limit );
713
		}
714
	}
715
716
	/**
717
	 * Activate the profiler (assuming $wgProfiler is set)
718
	 */
719
	protected function activateProfiler() {
720
		global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits;
721
722
		$output = $this->getOption( 'profiler' );
723
		if ( !$output ) {
724
			return;
725
		}
726
727
		if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) {
728
			$class = $wgProfiler['class'];
729
			/** @var Profiler $profiler */
730
			$profiler = new $class(
731
				[ 'sampling' => 1, 'output' => [ $output ] ]
732
					+ $wgProfiler
733
					+ [ 'threshold' => $wgProfileLimit ]
734
			);
735
			$profiler->setTemplated( true );
736
			Profiler::replaceStubInstance( $profiler );
737
		}
738
739
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
740
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
741
		$trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
742
	}
743
744
	/**
745
	 * Clear all params and arguments.
746
	 */
747
	public function clearParamsAndArgs() {
748
		$this->mOptions = [];
749
		$this->mArgs = [];
750
		$this->mInputLoaded = false;
751
	}
752
753
	/**
754
	 * Load params and arguments from a given array
755
	 * of command-line arguments
756
	 *
757
	 * @since 1.27
758
	 * @param array $argv
759
	 */
760
	public function loadWithArgv( $argv ) {
761
		$options = [];
762
		$args = [];
763
		$this->orderedOptions = [];
764
765
		# Parse arguments
766
		for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
767
			if ( $arg == '--' ) {
768
				# End of options, remainder should be considered arguments
769
				$arg = next( $argv );
770
				while ( $arg !== false ) {
771
					$args[] = $arg;
772
					$arg = next( $argv );
773
				}
774
				break;
775
			} elseif ( substr( $arg, 0, 2 ) == '--' ) {
776
				# Long options
777
				$option = substr( $arg, 2 );
778
				if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
779
					$param = next( $argv );
780
					if ( $param === false ) {
781
						$this->error( "\nERROR: $option parameter needs a value after it\n" );
782
						$this->maybeHelp( true );
783
					}
784
785
					$this->setParam( $options, $option, $param );
786
				} else {
787
					$bits = explode( '=', $option, 2 );
788
					if ( count( $bits ) > 1 ) {
789
						$option = $bits[0];
790
						$param = $bits[1];
791
					} else {
792
						$param = 1;
793
					}
794
795
					$this->setParam( $options, $option, $param );
796
				}
797
			} elseif ( $arg == '-' ) {
798
				# Lonely "-", often used to indicate stdin or stdout.
799
				$args[] = $arg;
800
			} elseif ( substr( $arg, 0, 1 ) == '-' ) {
801
				# Short options
802
				$argLength = strlen( $arg );
803
				for ( $p = 1; $p < $argLength; $p++ ) {
804
					$option = $arg[$p];
805
					if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
806
						$option = $this->mShortParamsMap[$option];
807
					}
808
809
					if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
810
						$param = next( $argv );
811
						if ( $param === false ) {
812
							$this->error( "\nERROR: $option parameter needs a value after it\n" );
813
							$this->maybeHelp( true );
814
						}
815
						$this->setParam( $options, $option, $param );
816
					} else {
817
						$this->setParam( $options, $option, 1 );
818
					}
819
				}
820
			} else {
821
				$args[] = $arg;
822
			}
823
		}
824
825
		$this->mOptions = $options;
826
		$this->mArgs = $args;
827
		$this->loadSpecialVars();
828
		$this->mInputLoaded = true;
829
	}
830
831
	/**
832
	 * Helper function used solely by loadParamsAndArgs
833
	 * to prevent code duplication
834
	 *
835
	 * This sets the param in the options array based on
836
	 * whether or not it can be specified multiple times.
837
	 *
838
	 * @since 1.27
839
	 * @param array $options
840
	 * @param string $option
841
	 * @param mixed $value
842
	 */
843
	private function setParam( &$options, $option, $value ) {
844
		$this->orderedOptions[] = [ $option, $value ];
845
846
		if ( isset( $this->mParams[$option] ) ) {
847
			$multi = $this->mParams[$option]['multiOccurrence'];
848
		} else {
849
			$multi = false;
850
		}
851
		$exists = array_key_exists( $option, $options );
852
		if ( $multi && $exists ) {
853
			$options[$option][] = $value;
854
		} elseif ( $multi ) {
855
			$options[$option] = [ $value ];
856
		} elseif ( !$exists ) {
857
			$options[$option] = $value;
858
		} else {
859
			$this->error( "\nERROR: $option parameter given twice\n" );
860
			$this->maybeHelp( true );
861
		}
862
	}
863
864
	/**
865
	 * Process command line arguments
866
	 * $mOptions becomes an array with keys set to the option names
867
	 * $mArgs becomes a zero-based array containing the non-option arguments
868
	 *
869
	 * @param string $self The name of the script, if any
870
	 * @param array $opts An array of options, in form of key=>value
871
	 * @param array $args An array of command line arguments
872
	 */
873
	public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
874
		# If we were given opts or args, set those and return early
875
		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...
876
			$this->mSelf = $self;
877
			$this->mInputLoaded = true;
878
		}
879
		if ( $opts ) {
880
			$this->mOptions = $opts;
881
			$this->mInputLoaded = true;
882
		}
883
		if ( $args ) {
884
			$this->mArgs = $args;
885
			$this->mInputLoaded = true;
886
		}
887
888
		# If we've already loaded input (either by user values or from $argv)
889
		# skip on loading it again. The array_shift() will corrupt values if
890
		# it's run again and again
891
		if ( $this->mInputLoaded ) {
892
			$this->loadSpecialVars();
893
894
			return;
895
		}
896
897
		global $argv;
898
		$this->mSelf = $argv[0];
899
		$this->loadWithArgv( array_slice( $argv, 1 ) );
900
	}
901
902
	/**
903
	 * Run some validation checks on the params, etc
904
	 */
905
	protected function validateParamsAndArgs() {
906
		$die = false;
907
		# Check to make sure we've got all the required options
908
		foreach ( $this->mParams as $opt => $info ) {
909
			if ( $info['require'] && !$this->hasOption( $opt ) ) {
910
				$this->error( "Param $opt required!" );
911
				$die = true;
912
			}
913
		}
914
		# Check arg list too
915
		foreach ( $this->mArgList as $k => $info ) {
916
			if ( $info['require'] && !$this->hasArg( $k ) ) {
917
				$this->error( 'Argument <' . $info['name'] . '> required!' );
918
				$die = true;
919
			}
920
		}
921
922
		if ( $die ) {
923
			$this->maybeHelp( true );
924
		}
925
	}
926
927
	/**
928
	 * Handle the special variables that are global to all scripts
929
	 */
930
	protected function loadSpecialVars() {
931
		if ( $this->hasOption( 'dbuser' ) ) {
932
			$this->mDbUser = $this->getOption( 'dbuser' );
933
		}
934
		if ( $this->hasOption( 'dbpass' ) ) {
935
			$this->mDbPass = $this->getOption( 'dbpass' );
936
		}
937
		if ( $this->hasOption( 'quiet' ) ) {
938
			$this->mQuiet = true;
939
		}
940
		if ( $this->hasOption( 'batch-size' ) ) {
941
			$this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
942
		}
943
	}
944
945
	/**
946
	 * Maybe show the help.
947
	 * @param bool $force Whether to force the help to show, default false
948
	 */
949
	protected function maybeHelp( $force = false ) {
950
		if ( !$force && !$this->hasOption( 'help' ) ) {
951
			return;
952
		}
953
954
		$screenWidth = 80; // TODO: Calculate this!
955
		$tab = "    ";
956
		$descWidth = $screenWidth - ( 2 * strlen( $tab ) );
957
958
		ksort( $this->mParams );
959
		$this->mQuiet = false;
960
961
		// Description ...
962
		if ( $this->mDescription ) {
963
			$this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
964
		}
965
		$output = "\nUsage: php " . basename( $this->mSelf );
966
967
		// ... append parameters ...
968
		if ( $this->mParams ) {
969
			$output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
970
		}
971
972
		// ... and append arguments.
973
		if ( $this->mArgList ) {
974
			$output .= ' ';
975
			foreach ( $this->mArgList as $k => $arg ) {
976
				if ( $arg['require'] ) {
977
					$output .= '<' . $arg['name'] . '>';
978
				} else {
979
					$output .= '[' . $arg['name'] . ']';
980
				}
981
				if ( $k < count( $this->mArgList ) - 1 ) {
982
					$output .= ' ';
983
				}
984
			}
985
		}
986
		$this->output( "$output\n\n" );
987
988
		# TODO abstract some repetitive code below
989
990
		// Generic parameters
991
		$this->output( "Generic maintenance parameters:\n" );
992
		foreach ( $this->mGenericParameters as $par => $info ) {
993
			if ( $info['shortName'] !== false ) {
994
				$par .= " (-{$info['shortName']})";
995
			}
996
			$this->output(
997
				wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
998
					"\n$tab$tab" ) . "\n"
999
			);
1000
		}
1001
		$this->output( "\n" );
1002
1003
		$scriptDependantParams = $this->mDependantParameters;
1004 View Code Duplication
		if ( count( $scriptDependantParams ) > 0 ) {
1005
			$this->output( "Script dependant parameters:\n" );
1006
			// Parameters description
1007
			foreach ( $scriptDependantParams as $par => $info ) {
1008
				if ( $info['shortName'] !== false ) {
1009
					$par .= " (-{$info['shortName']})";
1010
				}
1011
				$this->output(
1012
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1013
						"\n$tab$tab" ) . "\n"
1014
				);
1015
			}
1016
			$this->output( "\n" );
1017
		}
1018
1019
		// Script specific parameters not defined on construction by
1020
		// Maintenance::addDefaultParams()
1021
		$scriptSpecificParams = array_diff_key(
1022
			# all script parameters:
1023
			$this->mParams,
1024
			# remove the Maintenance default parameters:
1025
			$this->mGenericParameters,
1026
			$this->mDependantParameters
1027
		);
1028 View Code Duplication
		if ( count( $scriptSpecificParams ) > 0 ) {
1029
			$this->output( "Script specific parameters:\n" );
1030
			// Parameters description
1031
			foreach ( $scriptSpecificParams as $par => $info ) {
1032
				if ( $info['shortName'] !== false ) {
1033
					$par .= " (-{$info['shortName']})";
1034
				}
1035
				$this->output(
1036
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1037
						"\n$tab$tab" ) . "\n"
1038
				);
1039
			}
1040
			$this->output( "\n" );
1041
		}
1042
1043
		// Print arguments
1044
		if ( count( $this->mArgList ) > 0 ) {
1045
			$this->output( "Arguments:\n" );
1046
			// Arguments description
1047
			foreach ( $this->mArgList as $info ) {
1048
				$openChar = $info['require'] ? '<' : '[';
1049
				$closeChar = $info['require'] ? '>' : ']';
1050
				$this->output(
1051
					wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
1052
						$info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1053
				);
1054
			}
1055
			$this->output( "\n" );
1056
		}
1057
1058
		die( 1 );
0 ignored issues
show
Coding Style Compatibility introduced by
The method maybeHelp() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1059
	}
1060
1061
	/**
1062
	 * Handle some last-minute setup here.
1063
	 */
1064
	public function finalSetup() {
1065
		global $wgCommandLineMode, $wgShowSQLErrors, $wgServer;
1066
		global $wgDBadminuser, $wgDBadminpassword;
1067
		global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
1068
1069
		# Turn off output buffering again, it might have been turned on in the settings files
1070
		if ( ob_get_level() ) {
1071
			ob_end_flush();
1072
		}
1073
		# Same with these
1074
		$wgCommandLineMode = true;
1075
1076
		# Override $wgServer
1077
		if ( $this->hasOption( 'server' ) ) {
1078
			$wgServer = $this->getOption( 'server', $wgServer );
1079
		}
1080
1081
		# If these were passed, use them
1082
		if ( $this->mDbUser ) {
1083
			$wgDBadminuser = $this->mDbUser;
1084
		}
1085
		if ( $this->mDbPass ) {
1086
			$wgDBadminpassword = $this->mDbPass;
1087
		}
1088
1089
		if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1090
			$wgDBuser = $wgDBadminuser;
1091
			$wgDBpassword = $wgDBadminpassword;
1092
1093
			if ( $wgDBservers ) {
1094
				/**
1095
				 * @var $wgDBservers array
1096
				 */
1097
				foreach ( $wgDBservers as $i => $server ) {
1098
					$wgDBservers[$i]['user'] = $wgDBuser;
1099
					$wgDBservers[$i]['password'] = $wgDBpassword;
1100
				}
1101
			}
1102
			if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1103
				$wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1104
				$wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1105
			}
1106
			MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy();
1107
		}
1108
1109
		// Per-script profiling; useful for debugging
1110
		$this->activateProfiler();
1111
1112
		$this->afterFinalSetup();
1113
1114
		$wgShowSQLErrors = true;
1115
1116
		MediaWiki\suppressWarnings();
1117
		set_time_limit( 0 );
1118
		MediaWiki\restoreWarnings();
1119
1120
		$this->adjustMemoryLimit();
1121
	}
1122
1123
	/**
1124
	 * Execute a callback function at the end of initialisation
1125
	 */
1126
	protected function afterFinalSetup() {
1127
		if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1128
			call_user_func( MW_CMDLINE_CALLBACK );
1129
		}
1130
	}
1131
1132
	/**
1133
	 * Potentially debug globals. Originally a feature only
1134
	 * for refreshLinks
1135
	 */
1136
	public function globals() {
0 ignored issues
show
Coding Style introduced by
globals uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1137
		if ( $this->hasOption( 'globals' ) ) {
1138
			print_r( $GLOBALS );
1139
		}
1140
	}
1141
1142
	/**
1143
	 * Generic setup for most installs. Returns the location of LocalSettings
1144
	 * @return string
1145
	 */
1146
	public function loadSettings() {
1147
		global $wgCommandLineMode, $IP;
1148
1149
		if ( isset( $this->mOptions['conf'] ) ) {
1150
			$settingsFile = $this->mOptions['conf'];
1151
		} elseif ( defined( "MW_CONFIG_FILE" ) ) {
1152
			$settingsFile = MW_CONFIG_FILE;
1153
		} else {
1154
			$settingsFile = "$IP/LocalSettings.php";
1155
		}
1156
		if ( isset( $this->mOptions['wiki'] ) ) {
1157
			$bits = explode( '-', $this->mOptions['wiki'] );
1158
			if ( count( $bits ) == 1 ) {
1159
				$bits[] = '';
1160
			}
1161
			define( 'MW_DB', $bits[0] );
1162
			define( 'MW_PREFIX', $bits[1] );
1163
		}
1164
1165
		if ( !is_readable( $settingsFile ) ) {
1166
			$this->error( "A copy of your installation's LocalSettings.php\n" .
1167
				"must exist and be readable in the source directory.\n" .
1168
				"Use --conf to specify it.", true );
1169
		}
1170
		$wgCommandLineMode = true;
1171
1172
		return $settingsFile;
1173
	}
1174
1175
	/**
1176
	 * Support function for cleaning up redundant text records
1177
	 * @param bool $delete Whether or not to actually delete the records
1178
	 * @author Rob Church <[email protected]>
1179
	 */
1180
	public function purgeRedundantText( $delete = true ) {
1181
		# Data should come off the master, wrapped in a transaction
1182
		$dbw = $this->getDB( DB_MASTER );
1183
		$this->beginTransaction( $dbw, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by $this->getDB(DB_MASTER) on line 1182 can be null; however, Maintenance::beginTransaction() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1184
1185
		# Get "active" text records from the revisions table
1186
		$cur = [];
1187
		$this->output( 'Searching for active text records in revisions table...' );
1188
		$res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1189
		foreach ( $res as $row ) {
1190
			$cur[] = $row->rev_text_id;
1191
		}
1192
		$this->output( "done.\n" );
1193
1194
		# Get "active" text records from the archive table
1195
		$this->output( 'Searching for active text records in archive table...' );
1196
		$res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1197
		foreach ( $res as $row ) {
1198
			# old pre-MW 1.5 records can have null ar_text_id's.
1199
			if ( $row->ar_text_id !== null ) {
1200
				$cur[] = $row->ar_text_id;
1201
			}
1202
		}
1203
		$this->output( "done.\n" );
1204
1205
		# Get the IDs of all text records not in these sets
1206
		$this->output( 'Searching for inactive text records...' );
1207
		$cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1208
		$res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1209
		$old = [];
1210
		foreach ( $res as $row ) {
1211
			$old[] = $row->old_id;
1212
		}
1213
		$this->output( "done.\n" );
1214
1215
		# Inform the user of what we're going to do
1216
		$count = count( $old );
1217
		$this->output( "$count inactive items found.\n" );
1218
1219
		# Delete as appropriate
1220
		if ( $delete && $count ) {
1221
			$this->output( 'Deleting...' );
1222
			$dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1223
			$this->output( "done.\n" );
1224
		}
1225
1226
		# Done
1227
		$this->commitTransaction( $dbw, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by $this->getDB(DB_MASTER) on line 1182 can be null; however, Maintenance::commitTransaction() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1228
	}
1229
1230
	/**
1231
	 * Get the maintenance directory.
1232
	 * @return string
1233
	 */
1234
	protected function getDir() {
1235
		return __DIR__;
1236
	}
1237
1238
	/**
1239
	 * Returns a database to be used by current maintenance script. It can be set by setDB().
1240
	 * If not set, wfGetDB() will be used.
1241
	 * This function has the same parameters as wfGetDB()
1242
	 *
1243
	 * @param integer $db DB index (DB_REPLICA/DB_MASTER)
1244
	 * @param array $groups; default: empty array
0 ignored issues
show
Documentation introduced by
There is no parameter named $groups;. Did you maybe mean $groups?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1245
	 * @param string|bool $wiki; default: current wiki
0 ignored issues
show
Bug introduced by
There is no parameter named $wiki;. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1246
	 * @return Database
1247
	 */
1248
	protected function getDB( $db, $groups = [], $wiki = false ) {
1249
		if ( is_null( $this->mDb ) ) {
1250
			return wfGetDB( $db, $groups, $wiki );
1251
		} else {
1252
			return $this->mDb;
1253
		}
1254
	}
1255
1256
	/**
1257
	 * Sets database object to be returned by getDB().
1258
	 *
1259
	 * @param IDatabase $db Database object to be used
1260
	 */
1261
	public function setDB( IDatabase $db ) {
1262
		$this->mDb = $db;
0 ignored issues
show
Documentation Bug introduced by
$db is of type object<IDatabase>, but the property $mDb was declared to be of type object<Database>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
1263
	}
1264
1265
	/**
1266
	 * Begin a transcation on a DB
1267
	 *
1268
	 * This method makes it clear that begin() is called from a maintenance script,
1269
	 * which has outermost scope. This is safe, unlike $dbw->begin() called in other places.
1270
	 *
1271
	 * @param IDatabase $dbw
1272
	 * @param string $fname Caller name
1273
	 * @since 1.27
1274
	 */
1275
	protected function beginTransaction( IDatabase $dbw, $fname ) {
1276
		$dbw->begin( $fname );
1277
	}
1278
1279
	/**
1280
	 * Commit the transcation on a DB handle and wait for replica DBs to catch up
1281
	 *
1282
	 * This method makes it clear that commit() is called from a maintenance script,
1283
	 * which has outermost scope. This is safe, unlike $dbw->commit() called in other places.
1284
	 *
1285
	 * @param IDatabase $dbw
1286
	 * @param string $fname Caller name
1287
	 * @return bool Whether the replica DB wait succeeded
1288
	 * @since 1.27
1289
	 */
1290
	protected function commitTransaction( IDatabase $dbw, $fname ) {
1291
		$dbw->commit( $fname );
1292
		try {
1293
			$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1294
			$lbFactory->waitForReplication(
1295
				[ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1296
			);
1297
			$this->lastReplicationWait = microtime( true );
1298
1299
			return true;
1300
		} catch ( DBReplicationWaitError $e ) {
1301
			return false;
1302
		}
1303
	}
1304
1305
	/**
1306
	 * Rollback the transcation on a DB handle
1307
	 *
1308
	 * This method makes it clear that rollback() is called from a maintenance script,
1309
	 * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1310
	 *
1311
	 * @param IDatabase $dbw
1312
	 * @param string $fname Caller name
1313
	 * @since 1.27
1314
	 */
1315
	protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1316
		$dbw->rollback( $fname );
1317
	}
1318
1319
	/**
1320
	 * Lock the search index
1321
	 * @param Database &$db
1322
	 */
1323
	private function lockSearchindex( $db ) {
1324
		$write = [ 'searchindex' ];
1325
		$read = [
1326
			'page',
1327
			'revision',
1328
			'text',
1329
			'interwiki',
1330
			'l10n_cache',
1331
			'user',
1332
			'page_restrictions'
1333
		];
1334
		$db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1335
	}
1336
1337
	/**
1338
	 * Unlock the tables
1339
	 * @param Database &$db
1340
	 */
1341
	private function unlockSearchindex( $db ) {
1342
		$db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1343
	}
1344
1345
	/**
1346
	 * Unlock and lock again
1347
	 * Since the lock is low-priority, queued reads will be able to complete
1348
	 * @param Database &$db
1349
	 */
1350
	private function relockSearchindex( $db ) {
1351
		$this->unlockSearchindex( $db );
1352
		$this->lockSearchindex( $db );
1353
	}
1354
1355
	/**
1356
	 * Perform a search index update with locking
1357
	 * @param int $maxLockTime The maximum time to keep the search index locked.
1358
	 * @param string $callback The function that will update the function.
1359
	 * @param Database $dbw
1360
	 * @param array $results
1361
	 */
1362
	public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1363
		$lockTime = time();
1364
1365
		# Lock searchindex
1366
		if ( $maxLockTime ) {
1367
			$this->output( "   --- Waiting for lock ---" );
1368
			$this->lockSearchindex( $dbw );
1369
			$lockTime = time();
1370
			$this->output( "\n" );
1371
		}
1372
1373
		# Loop through the results and do a search update
1374
		foreach ( $results as $row ) {
1375
			# Allow reads to be processed
1376
			if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1377
				$this->output( "    --- Relocking ---" );
1378
				$this->relockSearchindex( $dbw );
1379
				$lockTime = time();
1380
				$this->output( "\n" );
1381
			}
1382
			call_user_func( $callback, $dbw, $row );
1383
		}
1384
1385
		# Unlock searchindex
1386
		if ( $maxLockTime ) {
1387
			$this->output( "    --- Unlocking --" );
1388
			$this->unlockSearchindex( $dbw );
1389
			$this->output( "\n" );
1390
		}
1391
	}
1392
1393
	/**
1394
	 * Update the searchindex table for a given pageid
1395
	 * @param Database $dbw A database write handle
1396
	 * @param int $pageId The page ID to update.
1397
	 * @return null|string
1398
	 */
1399
	public function updateSearchIndexForPage( $dbw, $pageId ) {
1400
		// Get current revision
1401
		$rev = Revision::loadFromPageId( $dbw, $pageId );
1402
		$title = null;
1403
		if ( $rev ) {
1404
			$titleObj = $rev->getTitle();
1405
			$title = $titleObj->getPrefixedDBkey();
1406
			$this->output( "$title..." );
1407
			# Update searchindex
1408
			$u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() );
0 ignored issues
show
Bug introduced by
It seems like $rev->getContent() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1409
			$u->doUpdate();
1410
			$this->output( "\n" );
1411
		}
1412
1413
		return $title;
1414
	}
1415
1416
	/**
1417
	 * Wrapper for posix_isatty()
1418
	 * We default as considering stdin a tty (for nice readline methods)
1419
	 * but treating stout as not a tty to avoid color codes
1420
	 *
1421
	 * @param mixed $fd File descriptor
1422
	 * @return bool
1423
	 */
1424
	public static function posix_isatty( $fd ) {
1425
		if ( !function_exists( 'posix_isatty' ) ) {
1426
			return !$fd;
1427
		} else {
1428
			return posix_isatty( $fd );
1429
		}
1430
	}
1431
1432
	/**
1433
	 * Prompt the console for input
1434
	 * @param string $prompt What to begin the line with, like '> '
1435
	 * @return string Response
1436
	 */
1437
	public static function readconsole( $prompt = '> ' ) {
1438
		static $isatty = null;
1439
		if ( is_null( $isatty ) ) {
1440
			$isatty = self::posix_isatty( 0 /*STDIN*/ );
1441
		}
1442
1443
		if ( $isatty && function_exists( 'readline' ) ) {
1444
			$resp = readline( $prompt );
1445
			if ( $resp === null ) {
1446
				// Workaround for https://github.com/facebook/hhvm/issues/4776
1447
				return false;
1448
			} else {
1449
				return $resp;
1450
			}
1451
		} else {
1452
			if ( $isatty ) {
1453
				$st = self::readlineEmulation( $prompt );
1454
			} else {
1455
				if ( feof( STDIN ) ) {
1456
					$st = false;
1457
				} else {
1458
					$st = fgets( STDIN, 1024 );
1459
				}
1460
			}
1461
			if ( $st === false ) {
1462
				return false;
1463
			}
1464
			$resp = trim( $st );
1465
1466
			return $resp;
1467
		}
1468
	}
1469
1470
	/**
1471
	 * Emulate readline()
1472
	 * @param string $prompt What to begin the line with, like '> '
1473
	 * @return string
1474
	 */
1475
	private static function readlineEmulation( $prompt ) {
1476
		$bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1477
		if ( !wfIsWindows() && $bash ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $bash of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
1478
			$retval = false;
1479
			$encPrompt = wfEscapeShellArg( $prompt );
1480
			$command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1481
			$encCommand = wfEscapeShellArg( $command );
1482
			$line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1483
1484
			if ( $retval == 0 ) {
1485
				return $line;
1486
			} elseif ( $retval == 127 ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
1487
				// Couldn't execute bash even though we thought we saw it.
1488
				// Shell probably spit out an error message, sorry :(
1489
				// Fall through to fgets()...
1490
			} else {
1491
				// EOF/ctrl+D
1492
				return false;
1493
			}
1494
		}
1495
1496
		// Fallback... we'll have no editing controls, EWWW
1497
		if ( feof( STDIN ) ) {
1498
			return false;
1499
		}
1500
		print $prompt;
1501
1502
		return fgets( STDIN, 1024 );
1503
	}
1504
1505
	/**
1506
	 * Get the terminal size as a two-element array where the first element
1507
	 * is the width (number of columns) and the second element is the height
1508
	 * (number of rows).
1509
	 *
1510
	 * @return array
1511
	 */
1512
	public static function getTermSize() {
1513
		$default = [ 80, 50 ];
1514
		if ( wfIsWindows() ) {
1515
			return $default;
1516
		}
1517
		// It's possible to get the screen size with VT-100 terminal escapes,
1518
		// but reading the responses is not possible without setting raw mode
1519
		// (unless you want to require the user to press enter), and that
1520
		// requires an ioctl(), which we can't do. So we have to shell out to
1521
		// something that can do the relevant syscalls. There are a few
1522
		// options. Linux and Mac OS X both have "stty size" which does the
1523
		// job directly.
1524
		$retval = false;
1525
		$size = wfShellExec( 'stty size', $retval );
1526
		if ( $retval !== 0 ) {
1527
			return $default;
1528
		}
1529
		if ( !preg_match( '/^(\d+) (\d+)$/', $size, $m ) ) {
1530
			return $default;
1531
		}
1532
		return [ intval( $m[2] ), intval( $m[1] ) ];
1533
	}
1534
1535
	/**
1536
	 * Call this to set up the autoloader to allow classes to be used from the
1537
	 * tests directory.
1538
	 */
1539
	public static function requireTestsAutoloader() {
1540
		require_once __DIR__ . '/../tests/common/TestsAutoLoader.php';
1541
	}
1542
}
1543
1544
/**
1545
 * Fake maintenance wrapper, mostly used for the web installer/updater
1546
 */
1547
class FakeMaintenance extends Maintenance {
1548
	protected $mSelf = "FakeMaintenanceScript";
1549
1550
	public function execute() {
1551
		return;
1552
	}
1553
}
1554
1555
/**
1556
 * Class for scripts that perform database maintenance and want to log the
1557
 * update in `updatelog` so we can later skip it
1558
 */
1559
abstract class LoggedUpdateMaintenance extends Maintenance {
1560
	public function __construct() {
1561
		parent::__construct();
1562
		$this->addOption( 'force', 'Run the update even if it was completed already' );
1563
		$this->setBatchSize( 200 );
1564
	}
1565
1566
	public function execute() {
1567
		$db = $this->getDB( DB_MASTER );
1568
		$key = $this->getUpdateKey();
1569
1570
		if ( !$this->hasOption( 'force' )
1571
			&& $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1572
		) {
1573
			$this->output( "..." . $this->updateSkippedMessage() . "\n" );
1574
1575
			return true;
1576
		}
1577
1578
		if ( !$this->doDBUpdates() ) {
1579
			return false;
1580
		}
1581
1582 View Code Duplication
		if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1583
			return true;
1584
		} else {
1585
			$this->output( $this->updatelogFailedMessage() . "\n" );
1586
1587
			return false;
1588
		}
1589
	}
1590
1591
	/**
1592
	 * Message to show that the update was done already and was just skipped
1593
	 * @return string
1594
	 */
1595
	protected function updateSkippedMessage() {
1596
		$key = $this->getUpdateKey();
1597
1598
		return "Update '{$key}' already logged as completed.";
1599
	}
1600
1601
	/**
1602
	 * Message to show that the update log was unable to log the completion of this update
1603
	 * @return string
1604
	 */
1605
	protected function updatelogFailedMessage() {
1606
		$key = $this->getUpdateKey();
1607
1608
		return "Unable to log update '{$key}' as completed.";
1609
	}
1610
1611
	/**
1612
	 * Do the actual work. All child classes will need to implement this.
1613
	 * Return true to log the update as done or false (usually on failure).
1614
	 * @return bool
1615
	 */
1616
	abstract protected function doDBUpdates();
1617
1618
	/**
1619
	 * Get the update key name to go in the update log table
1620
	 * @return string
1621
	 */
1622
	abstract protected function getUpdateKey();
1623
}
1624