Completed
Branch master (4cbefc)
by
unknown
27:08
created

Maintenance::requireTestsAutoloader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
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 IDatabase
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 setTriggers() {
557
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
558
		self::setLBFactoryTriggers( $lbFactory );
559
	}
560
561
	/**
562
	 * @param LBFactory $LBFactory
563
	 * @since 1.28
564
	 */
565
	public static function setLBFactoryTriggers( LBFactory $LBFactory ) {
566
		// Hook into period lag checks which often happen in long-running scripts
567
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
568
		$lbFactory->setWaitForReplicationListener(
569
			__METHOD__,
570
			function () {
571
				global $wgCommandLineMode;
572
				// Check config in case of JobRunner and unit tests
573
				if ( $wgCommandLineMode ) {
574
					DeferredUpdates::tryOpportunisticExecute( 'run' );
575
				}
576
			}
577
		);
578
		// Check for other windows to run them. A script may read or do a few writes
579
		// to the master but mostly be writing to something else, like a file store.
580
		$lbFactory->getMainLB()->setTransactionListener(
581
			__METHOD__,
582
			function ( $trigger ) {
583
				global $wgCommandLineMode;
584
				// Check config in case of JobRunner and unit tests
585
				if ( $wgCommandLineMode && $trigger === IDatabase::TRIGGER_COMMIT ) {
586
					DeferredUpdates::tryOpportunisticExecute( 'run' );
587
				}
588
			}
589
		);
590
	}
591
592
	/**
593
	 * Run a child maintenance script. Pass all of the current arguments
594
	 * to it.
595
	 * @param string $maintClass A name of a child maintenance class
596
	 * @param string $classFile Full path of where the child is
597
	 * @return Maintenance
598
	 */
599
	public function runChild( $maintClass, $classFile = null ) {
600
		// Make sure the class is loaded first
601
		if ( !class_exists( $maintClass ) ) {
602
			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...
603
				require_once $classFile;
604
			}
605
			if ( !class_exists( $maintClass ) ) {
606
				$this->error( "Cannot spawn child: $maintClass" );
607
			}
608
		}
609
610
		/**
611
		 * @var $child Maintenance
612
		 */
613
		$child = new $maintClass();
614
		$child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
615
		if ( !is_null( $this->mDb ) ) {
616
			$child->setDB( $this->mDb );
617
		}
618
619
		return $child;
620
	}
621
622
	/**
623
	 * Do some sanity checking and basic setup
624
	 */
625
	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...
626
		global $IP, $wgCommandLineMode, $wgRequestTime;
627
628
		# Abort if called from a web server
629
		if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
630
			$this->error( 'This script must be run from the command line', true );
631
		}
632
633
		if ( $IP === null ) {
634
			$this->error( "\$IP not set, aborting!\n" .
635
				'(Did you forget to call parent::__construct() in your maintenance script?)', 1 );
636
		}
637
638
		# Make sure we can handle script parameters
639
		if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
640
			$this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
641
		}
642
643
		// Send PHP warnings and errors to stderr instead of stdout.
644
		// This aids in diagnosing problems, while keeping messages
645
		// out of redirected output.
646
		if ( ini_get( 'display_errors' ) ) {
647
			ini_set( 'display_errors', 'stderr' );
648
		}
649
650
		$this->loadParamsAndArgs();
651
		$this->maybeHelp();
652
653
		# Set the memory limit
654
		# Note we need to set it again later in cache LocalSettings changed it
655
		$this->adjustMemoryLimit();
656
657
		# Set max execution time to 0 (no limit). PHP.net says that
658
		# "When running PHP from the command line the default setting is 0."
659
		# But sometimes this doesn't seem to be the case.
660
		ini_set( 'max_execution_time', 0 );
661
662
		$wgRequestTime = microtime( true );
663
664
		# Define us as being in MediaWiki
665
		define( 'MEDIAWIKI', true );
666
667
		$wgCommandLineMode = true;
668
669
		# Turn off output buffering if it's on
670
		while ( ob_get_level() > 0 ) {
671
			ob_end_flush();
672
		}
673
674
		$this->validateParamsAndArgs();
675
	}
676
677
	/**
678
	 * Normally we disable the memory_limit when running admin scripts.
679
	 * Some scripts may wish to actually set a limit, however, to avoid
680
	 * blowing up unexpectedly. We also support a --memory-limit option,
681
	 * to allow sysadmins to explicitly set one if they'd prefer to override
682
	 * defaults (or for people using Suhosin which yells at you for trying
683
	 * to disable the limits)
684
	 * @return string
685
	 */
686
	public function memoryLimit() {
687
		$limit = $this->getOption( 'memory-limit', 'max' );
688
		$limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
689
		return $limit;
690
	}
691
692
	/**
693
	 * Adjusts PHP's memory limit to better suit our needs, if needed.
694
	 */
695
	protected function adjustMemoryLimit() {
696
		$limit = $this->memoryLimit();
697
		if ( $limit == 'max' ) {
698
			$limit = -1; // no memory limit
699
		}
700
		if ( $limit != 'default' ) {
701
			ini_set( 'memory_limit', $limit );
702
		}
703
	}
704
705
	/**
706
	 * Activate the profiler (assuming $wgProfiler is set)
707
	 */
708
	protected function activateProfiler() {
709
		global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits;
710
711
		$output = $this->getOption( 'profiler' );
712
		if ( !$output ) {
713
			return;
714
		}
715
716
		if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) {
717
			$class = $wgProfiler['class'];
718
			$profiler = new $class(
719
				[ 'sampling' => 1, 'output' => [ $output ] ]
720
					+ $wgProfiler
721
					+ [ 'threshold' => $wgProfileLimit ]
722
			);
723
			$profiler->setTemplated( true );
724
			Profiler::replaceStubInstance( $profiler );
725
		}
726
727
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
728
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
729
		$trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
730
	}
731
732
	/**
733
	 * Clear all params and arguments.
734
	 */
735
	public function clearParamsAndArgs() {
736
		$this->mOptions = [];
737
		$this->mArgs = [];
738
		$this->mInputLoaded = false;
739
	}
740
741
	/**
742
	 * Load params and arguments from a given array
743
	 * of command-line arguments
744
	 *
745
	 * @since 1.27
746
	 * @param array $argv
747
	 */
748
	public function loadWithArgv( $argv ) {
749
		$options = [];
750
		$args = [];
751
		$this->orderedOptions = [];
752
753
		# Parse arguments
754
		for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
755
			if ( $arg == '--' ) {
756
				# End of options, remainder should be considered arguments
757
				$arg = next( $argv );
758
				while ( $arg !== false ) {
759
					$args[] = $arg;
760
					$arg = next( $argv );
761
				}
762
				break;
763
			} elseif ( substr( $arg, 0, 2 ) == '--' ) {
764
				# Long options
765
				$option = substr( $arg, 2 );
766
				if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
767
					$param = next( $argv );
768
					if ( $param === false ) {
769
						$this->error( "\nERROR: $option parameter needs a value after it\n" );
770
						$this->maybeHelp( true );
771
					}
772
773
					$this->setParam( $options, $option, $param );
774
				} else {
775
					$bits = explode( '=', $option, 2 );
776
					if ( count( $bits ) > 1 ) {
777
						$option = $bits[0];
778
						$param = $bits[1];
779
					} else {
780
						$param = 1;
781
					}
782
783
					$this->setParam( $options, $option, $param );
784
				}
785
			} elseif ( $arg == '-' ) {
786
				# Lonely "-", often used to indicate stdin or stdout.
787
				$args[] = $arg;
788
			} elseif ( substr( $arg, 0, 1 ) == '-' ) {
789
				# Short options
790
				$argLength = strlen( $arg );
791
				for ( $p = 1; $p < $argLength; $p++ ) {
792
					$option = $arg[$p];
793
					if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
794
						$option = $this->mShortParamsMap[$option];
795
					}
796
797
					if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
798
						$param = next( $argv );
799
						if ( $param === false ) {
800
							$this->error( "\nERROR: $option parameter needs a value after it\n" );
801
							$this->maybeHelp( true );
802
						}
803
						$this->setParam( $options, $option, $param );
804
					} else {
805
						$this->setParam( $options, $option, 1 );
806
					}
807
				}
808
			} else {
809
				$args[] = $arg;
810
			}
811
		}
812
813
		$this->mOptions = $options;
814
		$this->mArgs = $args;
815
		$this->loadSpecialVars();
816
		$this->mInputLoaded = true;
817
	}
818
819
	/**
820
	 * Helper function used solely by loadParamsAndArgs
821
	 * to prevent code duplication
822
	 *
823
	 * This sets the param in the options array based on
824
	 * whether or not it can be specified multiple times.
825
	 *
826
	 * @since 1.27
827
	 * @param array $options
828
	 * @param string $option
829
	 * @param mixed $value
830
	 */
831
	private function setParam( &$options, $option, $value ) {
832
		$this->orderedOptions[] = [ $option, $value ];
833
834
		if ( isset( $this->mParams[$option] ) ) {
835
			$multi = $this->mParams[$option]['multiOccurrence'];
836
		} else {
837
			$multi = false;
838
		}
839
		$exists = array_key_exists( $option, $options );
840
		if ( $multi && $exists ) {
841
			$options[$option][] = $value;
842
		} elseif ( $multi ) {
843
			$options[$option] = [ $value ];
844
		} elseif ( !$exists ) {
845
			$options[$option] = $value;
846
		} else {
847
			$this->error( "\nERROR: $option parameter given twice\n" );
848
			$this->maybeHelp( true );
849
		}
850
	}
851
852
	/**
853
	 * Process command line arguments
854
	 * $mOptions becomes an array with keys set to the option names
855
	 * $mArgs becomes a zero-based array containing the non-option arguments
856
	 *
857
	 * @param string $self The name of the script, if any
858
	 * @param array $opts An array of options, in form of key=>value
859
	 * @param array $args An array of command line arguments
860
	 */
861
	public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
862
		# If we were given opts or args, set those and return early
863
		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...
864
			$this->mSelf = $self;
865
			$this->mInputLoaded = true;
866
		}
867
		if ( $opts ) {
868
			$this->mOptions = $opts;
869
			$this->mInputLoaded = true;
870
		}
871
		if ( $args ) {
872
			$this->mArgs = $args;
873
			$this->mInputLoaded = true;
874
		}
875
876
		# If we've already loaded input (either by user values or from $argv)
877
		# skip on loading it again. The array_shift() will corrupt values if
878
		# it's run again and again
879
		if ( $this->mInputLoaded ) {
880
			$this->loadSpecialVars();
881
882
			return;
883
		}
884
885
		global $argv;
886
		$this->mSelf = $argv[0];
887
		$this->loadWithArgv( array_slice( $argv, 1 ) );
888
	}
889
890
	/**
891
	 * Run some validation checks on the params, etc
892
	 */
893
	protected function validateParamsAndArgs() {
894
		$die = false;
895
		# Check to make sure we've got all the required options
896
		foreach ( $this->mParams as $opt => $info ) {
897
			if ( $info['require'] && !$this->hasOption( $opt ) ) {
898
				$this->error( "Param $opt required!" );
899
				$die = true;
900
			}
901
		}
902
		# Check arg list too
903
		foreach ( $this->mArgList as $k => $info ) {
904
			if ( $info['require'] && !$this->hasArg( $k ) ) {
905
				$this->error( 'Argument <' . $info['name'] . '> required!' );
906
				$die = true;
907
			}
908
		}
909
910
		if ( $die ) {
911
			$this->maybeHelp( true );
912
		}
913
	}
914
915
	/**
916
	 * Handle the special variables that are global to all scripts
917
	 */
918
	protected function loadSpecialVars() {
919
		if ( $this->hasOption( 'dbuser' ) ) {
920
			$this->mDbUser = $this->getOption( 'dbuser' );
921
		}
922
		if ( $this->hasOption( 'dbpass' ) ) {
923
			$this->mDbPass = $this->getOption( 'dbpass' );
924
		}
925
		if ( $this->hasOption( 'quiet' ) ) {
926
			$this->mQuiet = true;
927
		}
928
		if ( $this->hasOption( 'batch-size' ) ) {
929
			$this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
930
		}
931
	}
932
933
	/**
934
	 * Maybe show the help.
935
	 * @param bool $force Whether to force the help to show, default false
936
	 */
937
	protected function maybeHelp( $force = false ) {
938
		if ( !$force && !$this->hasOption( 'help' ) ) {
939
			return;
940
		}
941
942
		$screenWidth = 80; // TODO: Calculate this!
943
		$tab = "    ";
944
		$descWidth = $screenWidth - ( 2 * strlen( $tab ) );
945
946
		ksort( $this->mParams );
947
		$this->mQuiet = false;
948
949
		// Description ...
950
		if ( $this->mDescription ) {
951
			$this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
952
		}
953
		$output = "\nUsage: php " . basename( $this->mSelf );
954
955
		// ... append parameters ...
956
		if ( $this->mParams ) {
957
			$output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
958
		}
959
960
		// ... and append arguments.
961
		if ( $this->mArgList ) {
962
			$output .= ' ';
963
			foreach ( $this->mArgList as $k => $arg ) {
964
				if ( $arg['require'] ) {
965
					$output .= '<' . $arg['name'] . '>';
966
				} else {
967
					$output .= '[' . $arg['name'] . ']';
968
				}
969
				if ( $k < count( $this->mArgList ) - 1 ) {
970
					$output .= ' ';
971
				}
972
			}
973
		}
974
		$this->output( "$output\n\n" );
975
976
		# TODO abstract some repetitive code below
977
978
		// Generic parameters
979
		$this->output( "Generic maintenance parameters:\n" );
980
		foreach ( $this->mGenericParameters as $par => $info ) {
981
			if ( $info['shortName'] !== false ) {
982
				$par .= " (-{$info['shortName']})";
983
			}
984
			$this->output(
985
				wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
986
					"\n$tab$tab" ) . "\n"
987
			);
988
		}
989
		$this->output( "\n" );
990
991
		$scriptDependantParams = $this->mDependantParameters;
992 View Code Duplication
		if ( count( $scriptDependantParams ) > 0 ) {
993
			$this->output( "Script dependant parameters:\n" );
994
			// Parameters description
995
			foreach ( $scriptDependantParams as $par => $info ) {
996
				if ( $info['shortName'] !== false ) {
997
					$par .= " (-{$info['shortName']})";
998
				}
999
				$this->output(
1000
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1001
						"\n$tab$tab" ) . "\n"
1002
				);
1003
			}
1004
			$this->output( "\n" );
1005
		}
1006
1007
		// Script specific parameters not defined on construction by
1008
		// Maintenance::addDefaultParams()
1009
		$scriptSpecificParams = array_diff_key(
1010
			# all script parameters:
1011
			$this->mParams,
1012
			# remove the Maintenance default parameters:
1013
			$this->mGenericParameters,
1014
			$this->mDependantParameters
1015
		);
1016 View Code Duplication
		if ( count( $scriptSpecificParams ) > 0 ) {
1017
			$this->output( "Script specific parameters:\n" );
1018
			// Parameters description
1019
			foreach ( $scriptSpecificParams as $par => $info ) {
1020
				if ( $info['shortName'] !== false ) {
1021
					$par .= " (-{$info['shortName']})";
1022
				}
1023
				$this->output(
1024
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1025
						"\n$tab$tab" ) . "\n"
1026
				);
1027
			}
1028
			$this->output( "\n" );
1029
		}
1030
1031
		// Print arguments
1032
		if ( count( $this->mArgList ) > 0 ) {
1033
			$this->output( "Arguments:\n" );
1034
			// Arguments description
1035
			foreach ( $this->mArgList as $info ) {
1036
				$openChar = $info['require'] ? '<' : '[';
1037
				$closeChar = $info['require'] ? '>' : ']';
1038
				$this->output(
1039
					wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
1040
						$info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1041
				);
1042
			}
1043
			$this->output( "\n" );
1044
		}
1045
1046
		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...
1047
	}
1048
1049
	/**
1050
	 * Handle some last-minute setup here.
1051
	 */
1052
	public function finalSetup() {
1053
		global $wgCommandLineMode, $wgShowSQLErrors, $wgServer;
1054
		global $wgDBadminuser, $wgDBadminpassword;
1055
		global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
1056
1057
		# Turn off output buffering again, it might have been turned on in the settings files
1058
		if ( ob_get_level() ) {
1059
			ob_end_flush();
1060
		}
1061
		# Same with these
1062
		$wgCommandLineMode = true;
1063
1064
		# Override $wgServer
1065
		if ( $this->hasOption( 'server' ) ) {
1066
			$wgServer = $this->getOption( 'server', $wgServer );
1067
		}
1068
1069
		# If these were passed, use them
1070
		if ( $this->mDbUser ) {
1071
			$wgDBadminuser = $this->mDbUser;
1072
		}
1073
		if ( $this->mDbPass ) {
1074
			$wgDBadminpassword = $this->mDbPass;
1075
		}
1076
1077
		if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1078
			$wgDBuser = $wgDBadminuser;
1079
			$wgDBpassword = $wgDBadminpassword;
1080
1081
			if ( $wgDBservers ) {
1082
				/**
1083
				 * @var $wgDBservers array
1084
				 */
1085
				foreach ( $wgDBservers as $i => $server ) {
1086
					$wgDBservers[$i]['user'] = $wgDBuser;
1087
					$wgDBservers[$i]['password'] = $wgDBpassword;
1088
				}
1089
			}
1090
			if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1091
				$wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1092
				$wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1093
			}
1094
			LBFactory::destroyInstance();
1095
		}
1096
1097
		// Per-script profiling; useful for debugging
1098
		$this->activateProfiler();
1099
1100
		$this->afterFinalSetup();
1101
1102
		$wgShowSQLErrors = true;
1103
1104
		MediaWiki\suppressWarnings();
1105
		set_time_limit( 0 );
1106
		MediaWiki\restoreWarnings();
1107
1108
		$this->adjustMemoryLimit();
1109
	}
1110
1111
	/**
1112
	 * Execute a callback function at the end of initialisation
1113
	 */
1114
	protected function afterFinalSetup() {
1115
		if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1116
			call_user_func( MW_CMDLINE_CALLBACK );
1117
		}
1118
	}
1119
1120
	/**
1121
	 * Potentially debug globals. Originally a feature only
1122
	 * for refreshLinks
1123
	 */
1124
	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...
1125
		if ( $this->hasOption( 'globals' ) ) {
1126
			print_r( $GLOBALS );
1127
		}
1128
	}
1129
1130
	/**
1131
	 * Generic setup for most installs. Returns the location of LocalSettings
1132
	 * @return string
1133
	 */
1134
	public function loadSettings() {
1135
		global $wgCommandLineMode, $IP;
1136
1137
		if ( isset( $this->mOptions['conf'] ) ) {
1138
			$settingsFile = $this->mOptions['conf'];
1139
		} elseif ( defined( "MW_CONFIG_FILE" ) ) {
1140
			$settingsFile = MW_CONFIG_FILE;
1141
		} else {
1142
			$settingsFile = "$IP/LocalSettings.php";
1143
		}
1144
		if ( isset( $this->mOptions['wiki'] ) ) {
1145
			$bits = explode( '-', $this->mOptions['wiki'] );
1146
			if ( count( $bits ) == 1 ) {
1147
				$bits[] = '';
1148
			}
1149
			define( 'MW_DB', $bits[0] );
1150
			define( 'MW_PREFIX', $bits[1] );
1151
		}
1152
1153
		if ( !is_readable( $settingsFile ) ) {
1154
			$this->error( "A copy of your installation's LocalSettings.php\n" .
1155
				"must exist and be readable in the source directory.\n" .
1156
				"Use --conf to specify it.", true );
1157
		}
1158
		$wgCommandLineMode = true;
1159
1160
		return $settingsFile;
1161
	}
1162
1163
	/**
1164
	 * Support function for cleaning up redundant text records
1165
	 * @param bool $delete Whether or not to actually delete the records
1166
	 * @author Rob Church <[email protected]>
1167
	 */
1168
	public function purgeRedundantText( $delete = true ) {
1169
		# Data should come off the master, wrapped in a transaction
1170
		$dbw = $this->getDB( DB_MASTER );
1171
		$this->beginTransaction( $dbw, __METHOD__ );
1172
1173
		# Get "active" text records from the revisions table
1174
		$this->output( 'Searching for active text records in revisions table...' );
1175
		$res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1176
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1177
			$cur[] = $row->rev_text_id;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$cur was never initialized. Although not strictly required by PHP, it is generally a good practice to add $cur = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1178
		}
1179
		$this->output( "done.\n" );
1180
1181
		# Get "active" text records from the archive table
1182
		$this->output( 'Searching for active text records in archive table...' );
1183
		$res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1184
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1185
			# old pre-MW 1.5 records can have null ar_text_id's.
1186
			if ( $row->ar_text_id !== null ) {
1187
				$cur[] = $row->ar_text_id;
0 ignored issues
show
Bug introduced by
The variable $cur does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1188
			}
1189
		}
1190
		$this->output( "done.\n" );
1191
1192
		# Get the IDs of all text records not in these sets
1193
		$this->output( 'Searching for inactive text records...' );
1194
		$cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1195
		$res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1196
		$old = [];
1197
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1198
			$old[] = $row->old_id;
1199
		}
1200
		$this->output( "done.\n" );
1201
1202
		# Inform the user of what we're going to do
1203
		$count = count( $old );
1204
		$this->output( "$count inactive items found.\n" );
1205
1206
		# Delete as appropriate
1207
		if ( $delete && $count ) {
1208
			$this->output( 'Deleting...' );
1209
			$dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1210
			$this->output( "done.\n" );
1211
		}
1212
1213
		# Done
1214
		$this->commitTransaction( $dbw, __METHOD__ );
1215
	}
1216
1217
	/**
1218
	 * Get the maintenance directory.
1219
	 * @return string
1220
	 */
1221
	protected function getDir() {
1222
		return __DIR__;
1223
	}
1224
1225
	/**
1226
	 * Returns a database to be used by current maintenance script. It can be set by setDB().
1227
	 * If not set, wfGetDB() will be used.
1228
	 * This function has the same parameters as wfGetDB()
1229
	 *
1230
	 * @param integer $db DB index (DB_REPLICA/DB_MASTER)
1231
	 * @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...
1232
	 * @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...
1233
	 * @return IDatabase
1234
	 */
1235
	protected function getDB( $db, $groups = [], $wiki = false ) {
1236
		if ( is_null( $this->mDb ) ) {
1237
			return wfGetDB( $db, $groups, $wiki );
1238
		} else {
1239
			return $this->mDb;
1240
		}
1241
	}
1242
1243
	/**
1244
	 * Sets database object to be returned by getDB().
1245
	 *
1246
	 * @param IDatabase $db Database object to be used
1247
	 */
1248
	public function setDB( IDatabase $db ) {
1249
		$this->mDb = $db;
1250
	}
1251
1252
	/**
1253
	 * Begin a transcation on a DB
1254
	 *
1255
	 * This method makes it clear that begin() is called from a maintenance script,
1256
	 * which has outermost scope. This is safe, unlike $dbw->begin() called in other places.
1257
	 *
1258
	 * @param IDatabase $dbw
1259
	 * @param string $fname Caller name
1260
	 * @since 1.27
1261
	 */
1262
	protected function beginTransaction( IDatabase $dbw, $fname ) {
1263
		$dbw->begin( $fname );
1264
	}
1265
1266
	/**
1267
	 * Commit the transcation on a DB handle and wait for replica DBs to catch up
1268
	 *
1269
	 * This method makes it clear that commit() is called from a maintenance script,
1270
	 * which has outermost scope. This is safe, unlike $dbw->commit() called in other places.
1271
	 *
1272
	 * @param IDatabase $dbw
1273
	 * @param string $fname Caller name
1274
	 * @return bool Whether the replica DB wait succeeded
1275
	 * @since 1.27
1276
	 */
1277
	protected function commitTransaction( IDatabase $dbw, $fname ) {
1278
		$dbw->commit( $fname );
1279
		try {
1280
			$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1281
			$lbFactory->waitForReplication(
1282
				[ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1283
			);
1284
			$this->lastReplicationWait = microtime( true );
1285
1286
			return true;
1287
		} catch ( DBReplicationWaitError $e ) {
1288
			return false;
1289
		}
1290
	}
1291
1292
	/**
1293
	 * Rollback the transcation on a DB handle
1294
	 *
1295
	 * This method makes it clear that rollback() is called from a maintenance script,
1296
	 * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1297
	 *
1298
	 * @param IDatabase $dbw
1299
	 * @param string $fname Caller name
1300
	 * @since 1.27
1301
	 */
1302
	protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1303
		$dbw->rollback( $fname );
1304
	}
1305
1306
	/**
1307
	 * Lock the search index
1308
	 * @param DatabaseBase &$db
1309
	 */
1310
	private function lockSearchindex( $db ) {
1311
		$write = [ 'searchindex' ];
1312
		$read = [
1313
			'page',
1314
			'revision',
1315
			'text',
1316
			'interwiki',
1317
			'l10n_cache',
1318
			'user',
1319
			'page_restrictions'
1320
		];
1321
		$db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1322
	}
1323
1324
	/**
1325
	 * Unlock the tables
1326
	 * @param DatabaseBase &$db
1327
	 */
1328
	private function unlockSearchindex( $db ) {
1329
		$db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1330
	}
1331
1332
	/**
1333
	 * Unlock and lock again
1334
	 * Since the lock is low-priority, queued reads will be able to complete
1335
	 * @param DatabaseBase &$db
1336
	 */
1337
	private function relockSearchindex( $db ) {
1338
		$this->unlockSearchindex( $db );
1339
		$this->lockSearchindex( $db );
1340
	}
1341
1342
	/**
1343
	 * Perform a search index update with locking
1344
	 * @param int $maxLockTime The maximum time to keep the search index locked.
1345
	 * @param string $callback The function that will update the function.
1346
	 * @param DatabaseBase $dbw
1347
	 * @param array $results
1348
	 */
1349
	public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1350
		$lockTime = time();
1351
1352
		# Lock searchindex
1353
		if ( $maxLockTime ) {
1354
			$this->output( "   --- Waiting for lock ---" );
1355
			$this->lockSearchindex( $dbw );
1356
			$lockTime = time();
1357
			$this->output( "\n" );
1358
		}
1359
1360
		# Loop through the results and do a search update
1361
		foreach ( $results as $row ) {
1362
			# Allow reads to be processed
1363
			if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1364
				$this->output( "    --- Relocking ---" );
1365
				$this->relockSearchindex( $dbw );
1366
				$lockTime = time();
1367
				$this->output( "\n" );
1368
			}
1369
			call_user_func( $callback, $dbw, $row );
1370
		}
1371
1372
		# Unlock searchindex
1373
		if ( $maxLockTime ) {
1374
			$this->output( "    --- Unlocking --" );
1375
			$this->unlockSearchindex( $dbw );
1376
			$this->output( "\n" );
1377
		}
1378
	}
1379
1380
	/**
1381
	 * Update the searchindex table for a given pageid
1382
	 * @param DatabaseBase $dbw A database write handle
1383
	 * @param int $pageId The page ID to update.
1384
	 * @return null|string
1385
	 */
1386
	public function updateSearchIndexForPage( $dbw, $pageId ) {
1387
		// Get current revision
1388
		$rev = Revision::loadFromPageId( $dbw, $pageId );
1389
		$title = null;
1390
		if ( $rev ) {
1391
			$titleObj = $rev->getTitle();
1392
			$title = $titleObj->getPrefixedDBkey();
1393
			$this->output( "$title..." );
1394
			# Update searchindex
1395
			$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...
1396
			$u->doUpdate();
1397
			$this->output( "\n" );
1398
		}
1399
1400
		return $title;
1401
	}
1402
1403
	/**
1404
	 * Wrapper for posix_isatty()
1405
	 * We default as considering stdin a tty (for nice readline methods)
1406
	 * but treating stout as not a tty to avoid color codes
1407
	 *
1408
	 * @param mixed $fd File descriptor
1409
	 * @return bool
1410
	 */
1411
	public static function posix_isatty( $fd ) {
1412
		if ( !function_exists( 'posix_isatty' ) ) {
1413
			return !$fd;
1414
		} else {
1415
			return posix_isatty( $fd );
1416
		}
1417
	}
1418
1419
	/**
1420
	 * Prompt the console for input
1421
	 * @param string $prompt What to begin the line with, like '> '
1422
	 * @return string Response
1423
	 */
1424
	public static function readconsole( $prompt = '> ' ) {
1425
		static $isatty = null;
1426
		if ( is_null( $isatty ) ) {
1427
			$isatty = self::posix_isatty( 0 /*STDIN*/ );
1428
		}
1429
1430
		if ( $isatty && function_exists( 'readline' ) ) {
1431
			$resp = readline( $prompt );
1432
			if ( $resp === null ) {
1433
				// Workaround for https://github.com/facebook/hhvm/issues/4776
1434
				return false;
1435
			} else {
1436
				return $resp;
1437
			}
1438
		} else {
1439
			if ( $isatty ) {
1440
				$st = self::readlineEmulation( $prompt );
1441
			} else {
1442
				if ( feof( STDIN ) ) {
1443
					$st = false;
1444
				} else {
1445
					$st = fgets( STDIN, 1024 );
1446
				}
1447
			}
1448
			if ( $st === false ) {
1449
				return false;
1450
			}
1451
			$resp = trim( $st );
1452
1453
			return $resp;
1454
		}
1455
	}
1456
1457
	/**
1458
	 * Emulate readline()
1459
	 * @param string $prompt What to begin the line with, like '> '
1460
	 * @return string
1461
	 */
1462
	private static function readlineEmulation( $prompt ) {
1463
		$bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1464
		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...
1465
			$retval = false;
1466
			$encPrompt = wfEscapeShellArg( $prompt );
1467
			$command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1468
			$encCommand = wfEscapeShellArg( $command );
1469
			$line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1470
1471
			if ( $retval == 0 ) {
1472
				return $line;
1473
			} 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...
1474
				// Couldn't execute bash even though we thought we saw it.
1475
				// Shell probably spit out an error message, sorry :(
1476
				// Fall through to fgets()...
1477
			} else {
1478
				// EOF/ctrl+D
1479
				return false;
1480
			}
1481
		}
1482
1483
		// Fallback... we'll have no editing controls, EWWW
1484
		if ( feof( STDIN ) ) {
1485
			return false;
1486
		}
1487
		print $prompt;
1488
1489
		return fgets( STDIN, 1024 );
1490
	}
1491
1492
	/**
1493
	 * Call this to set up the autoloader to allow classes to be used from the
1494
	 * tests directory.
1495
	 */
1496
	public static function requireTestsAutoloader() {
1497
		require_once __DIR__ . '/../tests/common/TestsAutoLoader.php';
1498
	}
1499
}
1500
1501
/**
1502
 * Fake maintenance wrapper, mostly used for the web installer/updater
1503
 */
1504
class FakeMaintenance extends Maintenance {
1505
	protected $mSelf = "FakeMaintenanceScript";
1506
1507
	public function execute() {
1508
		return;
1509
	}
1510
}
1511
1512
/**
1513
 * Class for scripts that perform database maintenance and want to log the
1514
 * update in `updatelog` so we can later skip it
1515
 */
1516
abstract class LoggedUpdateMaintenance extends Maintenance {
1517
	public function __construct() {
1518
		parent::__construct();
1519
		$this->addOption( 'force', 'Run the update even if it was completed already' );
1520
		$this->setBatchSize( 200 );
1521
	}
1522
1523
	public function execute() {
1524
		$db = $this->getDB( DB_MASTER );
1525
		$key = $this->getUpdateKey();
1526
1527
		if ( !$this->hasOption( 'force' )
1528
			&& $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1529
		) {
1530
			$this->output( "..." . $this->updateSkippedMessage() . "\n" );
1531
1532
			return true;
1533
		}
1534
1535
		if ( !$this->doDBUpdates() ) {
1536
			return false;
1537
		}
1538
1539 View Code Duplication
		if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1540
			return true;
1541
		} else {
1542
			$this->output( $this->updatelogFailedMessage() . "\n" );
1543
1544
			return false;
1545
		}
1546
	}
1547
1548
	/**
1549
	 * Message to show that the update was done already and was just skipped
1550
	 * @return string
1551
	 */
1552
	protected function updateSkippedMessage() {
1553
		$key = $this->getUpdateKey();
1554
1555
		return "Update '{$key}' already logged as completed.";
1556
	}
1557
1558
	/**
1559
	 * Message to show that the update log was unable to log the completion of this update
1560
	 * @return string
1561
	 */
1562
	protected function updatelogFailedMessage() {
1563
		$key = $this->getUpdateKey();
1564
1565
		return "Unable to log update '{$key}' as completed.";
1566
	}
1567
1568
	/**
1569
	 * Do the actual work. All child classes will need to implement this.
1570
	 * Return true to log the update as done or false (usually on failure).
1571
	 * @return bool
1572
	 */
1573
	abstract protected function doDBUpdates();
1574
1575
	/**
1576
	 * Get the update key name to go in the update log table
1577
	 * @return string
1578
	 */
1579
	abstract protected function getUpdateKey();
1580
}
1581