Completed
Branch master (4de667)
by
unknown
26:16
created

Maintenance::checkRequiredExtensions()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
1240
		$this->lastSlaveWait = microtime( true );
1241
1242
		return $ok;
1243
	}
1244
1245
	/**
1246
	 * Rollback the transcation on a DB handle
1247
	 *
1248
	 * This method makes it clear that rollback() is called from a maintenance script,
1249
	 * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1250
	 *
1251
	 * @param IDatabase $dbw
1252
	 * @param string $fname Caller name
1253
	 * @since 1.27
1254
	 */
1255
	protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1256
		$dbw->rollback( $fname );
1257
	}
1258
1259
	/**
1260
	 * Lock the search index
1261
	 * @param DatabaseBase &$db
1262
	 */
1263
	private function lockSearchindex( $db ) {
1264
		$write = [ 'searchindex' ];
1265
		$read = [
1266
			'page',
1267
			'revision',
1268
			'text',
1269
			'interwiki',
1270
			'l10n_cache',
1271
			'user',
1272
			'page_restrictions'
1273
		];
1274
		$db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1275
	}
1276
1277
	/**
1278
	 * Unlock the tables
1279
	 * @param DatabaseBase &$db
1280
	 */
1281
	private function unlockSearchindex( $db ) {
1282
		$db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1283
	}
1284
1285
	/**
1286
	 * Unlock and lock again
1287
	 * Since the lock is low-priority, queued reads will be able to complete
1288
	 * @param DatabaseBase &$db
1289
	 */
1290
	private function relockSearchindex( $db ) {
1291
		$this->unlockSearchindex( $db );
1292
		$this->lockSearchindex( $db );
1293
	}
1294
1295
	/**
1296
	 * Perform a search index update with locking
1297
	 * @param int $maxLockTime The maximum time to keep the search index locked.
1298
	 * @param string $callback The function that will update the function.
1299
	 * @param DatabaseBase $dbw
1300
	 * @param array $results
1301
	 */
1302
	public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1303
		$lockTime = time();
1304
1305
		# Lock searchindex
1306
		if ( $maxLockTime ) {
1307
			$this->output( "   --- Waiting for lock ---" );
1308
			$this->lockSearchindex( $dbw );
1309
			$lockTime = time();
1310
			$this->output( "\n" );
1311
		}
1312
1313
		# Loop through the results and do a search update
1314
		foreach ( $results as $row ) {
1315
			# Allow reads to be processed
1316
			if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1317
				$this->output( "    --- Relocking ---" );
1318
				$this->relockSearchindex( $dbw );
1319
				$lockTime = time();
1320
				$this->output( "\n" );
1321
			}
1322
			call_user_func( $callback, $dbw, $row );
1323
		}
1324
1325
		# Unlock searchindex
1326
		if ( $maxLockTime ) {
1327
			$this->output( "    --- Unlocking --" );
1328
			$this->unlockSearchindex( $dbw );
1329
			$this->output( "\n" );
1330
		}
1331
	}
1332
1333
	/**
1334
	 * Update the searchindex table for a given pageid
1335
	 * @param DatabaseBase $dbw A database write handle
1336
	 * @param int $pageId The page ID to update.
1337
	 * @return null|string
1338
	 */
1339
	public function updateSearchIndexForPage( $dbw, $pageId ) {
1340
		// Get current revision
1341
		$rev = Revision::loadFromPageId( $dbw, $pageId );
1342
		$title = null;
1343
		if ( $rev ) {
1344
			$titleObj = $rev->getTitle();
1345
			$title = $titleObj->getPrefixedDBkey();
1346
			$this->output( "$title..." );
1347
			# Update searchindex
1348
			$u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() );
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...
1349
			$u->doUpdate();
1350
			$this->output( "\n" );
1351
		}
1352
1353
		return $title;
1354
	}
1355
1356
	/**
1357
	 * Wrapper for posix_isatty()
1358
	 * We default as considering stdin a tty (for nice readline methods)
1359
	 * but treating stout as not a tty to avoid color codes
1360
	 *
1361
	 * @param mixed $fd File descriptor
1362
	 * @return bool
1363
	 */
1364
	public static function posix_isatty( $fd ) {
1365
		if ( !function_exists( 'posix_isatty' ) ) {
1366
			return !$fd;
1367
		} else {
1368
			return posix_isatty( $fd );
1369
		}
1370
	}
1371
1372
	/**
1373
	 * Prompt the console for input
1374
	 * @param string $prompt What to begin the line with, like '> '
1375
	 * @return string Response
1376
	 */
1377
	public static function readconsole( $prompt = '> ' ) {
1378
		static $isatty = null;
1379
		if ( is_null( $isatty ) ) {
1380
			$isatty = self::posix_isatty( 0 /*STDIN*/ );
1381
		}
1382
1383
		if ( $isatty && function_exists( 'readline' ) ) {
1384
			$resp = readline( $prompt );
1385
			if ( $resp === null ) {
1386
				// Workaround for https://github.com/facebook/hhvm/issues/4776
1387
				return false;
1388
			} else {
1389
				return $resp;
1390
			}
1391
		} else {
1392
			if ( $isatty ) {
1393
				$st = self::readlineEmulation( $prompt );
1394
			} else {
1395
				if ( feof( STDIN ) ) {
1396
					$st = false;
1397
				} else {
1398
					$st = fgets( STDIN, 1024 );
1399
				}
1400
			}
1401
			if ( $st === false ) {
1402
				return false;
1403
			}
1404
			$resp = trim( $st );
1405
1406
			return $resp;
1407
		}
1408
	}
1409
1410
	/**
1411
	 * Emulate readline()
1412
	 * @param string $prompt What to begin the line with, like '> '
1413
	 * @return string
1414
	 */
1415
	private static function readlineEmulation( $prompt ) {
1416
		$bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1417
		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...
1418
			$retval = false;
1419
			$encPrompt = wfEscapeShellArg( $prompt );
1420
			$command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1421
			$encCommand = wfEscapeShellArg( $command );
1422
			$line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1423
1424
			if ( $retval == 0 ) {
1425
				return $line;
1426
			} elseif ( $retval == 127 ) {
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...
1427
				// Couldn't execute bash even though we thought we saw it.
1428
				// Shell probably spit out an error message, sorry :(
1429
				// Fall through to fgets()...
1430
			} else {
1431
				// EOF/ctrl+D
1432
				return false;
1433
			}
1434
		}
1435
1436
		// Fallback... we'll have no editing controls, EWWW
1437
		if ( feof( STDIN ) ) {
1438
			return false;
1439
		}
1440
		print $prompt;
1441
1442
		return fgets( STDIN, 1024 );
1443
	}
1444
}
1445
1446
/**
1447
 * Fake maintenance wrapper, mostly used for the web installer/updater
1448
 */
1449
class FakeMaintenance extends Maintenance {
1450
	protected $mSelf = "FakeMaintenanceScript";
1451
1452
	public function execute() {
1453
		return;
1454
	}
1455
}
1456
1457
/**
1458
 * Class for scripts that perform database maintenance and want to log the
1459
 * update in `updatelog` so we can later skip it
1460
 */
1461
abstract class LoggedUpdateMaintenance extends Maintenance {
1462
	public function __construct() {
1463
		parent::__construct();
1464
		$this->addOption( 'force', 'Run the update even if it was completed already' );
1465
		$this->setBatchSize( 200 );
1466
	}
1467
1468
	public function execute() {
1469
		$db = $this->getDB( DB_MASTER );
1470
		$key = $this->getUpdateKey();
1471
1472
		if ( !$this->hasOption( 'force' )
1473
			&& $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1474
		) {
1475
			$this->output( "..." . $this->updateSkippedMessage() . "\n" );
1476
1477
			return true;
1478
		}
1479
1480
		if ( !$this->doDBUpdates() ) {
1481
			return false;
1482
		}
1483
1484 View Code Duplication
		if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1485
			return true;
1486
		} else {
1487
			$this->output( $this->updatelogFailedMessage() . "\n" );
1488
1489
			return false;
1490
		}
1491
	}
1492
1493
	/**
1494
	 * Message to show that the update was done already and was just skipped
1495
	 * @return string
1496
	 */
1497
	protected function updateSkippedMessage() {
1498
		$key = $this->getUpdateKey();
1499
1500
		return "Update '{$key}' already logged as completed.";
1501
	}
1502
1503
	/**
1504
	 * Message to show that the update log was unable to log the completion of this update
1505
	 * @return string
1506
	 */
1507
	protected function updatelogFailedMessage() {
1508
		$key = $this->getUpdateKey();
1509
1510
		return "Unable to log update '{$key}' as completed.";
1511
	}
1512
1513
	/**
1514
	 * Do the actual work. All child classes will need to implement this.
1515
	 * Return true to log the update as done or false (usually on failure).
1516
	 * @return bool
1517
	 */
1518
	abstract protected function doDBUpdates();
1519
1520
	/**
1521
	 * Get the update key name to go in the update log table
1522
	 * @return string
1523
	 */
1524
	abstract protected function getUpdateKey();
1525
}
1526