Completed
Branch master (dc3656)
by
unknown
30:14
created

Maintenance::maybeHelp()   D

Complexity

Conditions 21
Paths 193

Size

Total Lines 111
Code Lines 64

Duplication

Lines 28
Ratio 25.23 %

Importance

Changes 0
Metric Value
cc 21
eloc 64
nc 193
nop 1
dl 28
loc 111
rs 4.4233
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 $lastSlaveWait = 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
		// Hook into period lag checks which often happen in long-running scripts
558
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
559
		$lbFactory->setWaitForReplicationListener(
560
			__METHOD__,
561
			[ 'DeferredUpdates', 'tryOpportunisticExecute' ]
562
		);
563
	}
564
565
	/**
566
	 * Run a child maintenance script. Pass all of the current arguments
567
	 * to it.
568
	 * @param string $maintClass A name of a child maintenance class
569
	 * @param string $classFile Full path of where the child is
570
	 * @return Maintenance
571
	 */
572
	public function runChild( $maintClass, $classFile = null ) {
573
		// Make sure the class is loaded first
574
		if ( !class_exists( $maintClass ) ) {
575
			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...
576
				require_once $classFile;
577
			}
578
			if ( !class_exists( $maintClass ) ) {
579
				$this->error( "Cannot spawn child: $maintClass" );
580
			}
581
		}
582
583
		/**
584
		 * @var $child Maintenance
585
		 */
586
		$child = new $maintClass();
587
		$child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
588
		if ( !is_null( $this->mDb ) ) {
589
			$child->setDB( $this->mDb );
590
		}
591
592
		return $child;
593
	}
594
595
	/**
596
	 * Do some sanity checking and basic setup
597
	 */
598
	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...
599
		global $IP, $wgCommandLineMode, $wgRequestTime;
600
601
		# Abort if called from a web server
602
		if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
603
			$this->error( 'This script must be run from the command line', true );
604
		}
605
606
		if ( $IP === null ) {
607
			$this->error( "\$IP not set, aborting!\n" .
608
				'(Did you forget to call parent::__construct() in your maintenance script?)', 1 );
609
		}
610
611
		# Make sure we can handle script parameters
612
		if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
613
			$this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
614
		}
615
616
		// Send PHP warnings and errors to stderr instead of stdout.
617
		// This aids in diagnosing problems, while keeping messages
618
		// out of redirected output.
619
		if ( ini_get( 'display_errors' ) ) {
620
			ini_set( 'display_errors', 'stderr' );
621
		}
622
623
		$this->loadParamsAndArgs();
624
		$this->maybeHelp();
625
626
		# Set the memory limit
627
		# Note we need to set it again later in cache LocalSettings changed it
628
		$this->adjustMemoryLimit();
629
630
		# Set max execution time to 0 (no limit). PHP.net says that
631
		# "When running PHP from the command line the default setting is 0."
632
		# But sometimes this doesn't seem to be the case.
633
		ini_set( 'max_execution_time', 0 );
634
635
		$wgRequestTime = microtime( true );
636
637
		# Define us as being in MediaWiki
638
		define( 'MEDIAWIKI', true );
639
640
		$wgCommandLineMode = true;
641
642
		# Turn off output buffering if it's on
643
		while ( ob_get_level() > 0 ) {
644
			ob_end_flush();
645
		}
646
647
		$this->validateParamsAndArgs();
648
	}
649
650
	/**
651
	 * Normally we disable the memory_limit when running admin scripts.
652
	 * Some scripts may wish to actually set a limit, however, to avoid
653
	 * blowing up unexpectedly. We also support a --memory-limit option,
654
	 * to allow sysadmins to explicitly set one if they'd prefer to override
655
	 * defaults (or for people using Suhosin which yells at you for trying
656
	 * to disable the limits)
657
	 * @return string
658
	 */
659
	public function memoryLimit() {
660
		$limit = $this->getOption( 'memory-limit', 'max' );
661
		$limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
662
		return $limit;
663
	}
664
665
	/**
666
	 * Adjusts PHP's memory limit to better suit our needs, if needed.
667
	 */
668
	protected function adjustMemoryLimit() {
669
		$limit = $this->memoryLimit();
670
		if ( $limit == 'max' ) {
671
			$limit = -1; // no memory limit
672
		}
673
		if ( $limit != 'default' ) {
674
			ini_set( 'memory_limit', $limit );
675
		}
676
	}
677
678
	/**
679
	 * Activate the profiler (assuming $wgProfiler is set)
680
	 */
681
	protected function activateProfiler() {
682
		global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits;
683
684
		$output = $this->getOption( 'profiler' );
685
		if ( !$output ) {
686
			return;
687
		}
688
689
		if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) {
690
			$class = $wgProfiler['class'];
691
			$profiler = new $class(
692
				[ 'sampling' => 1, 'output' => [ $output ] ]
693
					+ $wgProfiler
694
					+ [ 'threshold' => $wgProfileLimit ]
695
			);
696
			$profiler->setTemplated( true );
697
			Profiler::replaceStubInstance( $profiler );
698
		}
699
700
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
701
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
702
		$trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
703
	}
704
705
	/**
706
	 * Clear all params and arguments.
707
	 */
708
	public function clearParamsAndArgs() {
709
		$this->mOptions = [];
710
		$this->mArgs = [];
711
		$this->mInputLoaded = false;
712
	}
713
714
	/**
715
	 * Load params and arguments from a given array
716
	 * of command-line arguments
717
	 *
718
	 * @since 1.27
719
	 * @param array $argv
720
	 */
721
	public function loadWithArgv( $argv ) {
722
		$options = [];
723
		$args = [];
724
		$this->orderedOptions = [];
725
726
		# Parse arguments
727
		for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
728
			if ( $arg == '--' ) {
729
				# End of options, remainder should be considered arguments
730
				$arg = next( $argv );
731
				while ( $arg !== false ) {
732
					$args[] = $arg;
733
					$arg = next( $argv );
734
				}
735
				break;
736
			} elseif ( substr( $arg, 0, 2 ) == '--' ) {
737
				# Long options
738
				$option = substr( $arg, 2 );
739
				if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
740
					$param = next( $argv );
741
					if ( $param === false ) {
742
						$this->error( "\nERROR: $option parameter needs a value after it\n" );
743
						$this->maybeHelp( true );
744
					}
745
746
					$this->setParam( $options, $option, $param );
747
				} else {
748
					$bits = explode( '=', $option, 2 );
749
					if ( count( $bits ) > 1 ) {
750
						$option = $bits[0];
751
						$param = $bits[1];
752
					} else {
753
						$param = 1;
754
					}
755
756
					$this->setParam( $options, $option, $param );
757
				}
758
			} elseif ( $arg == '-' ) {
759
				# Lonely "-", often used to indicate stdin or stdout.
760
				$args[] = $arg;
761
			} elseif ( substr( $arg, 0, 1 ) == '-' ) {
762
				# Short options
763
				$argLength = strlen( $arg );
764
				for ( $p = 1; $p < $argLength; $p++ ) {
765
					$option = $arg[$p];
766
					if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
767
						$option = $this->mShortParamsMap[$option];
768
					}
769
770
					if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
771
						$param = next( $argv );
772
						if ( $param === false ) {
773
							$this->error( "\nERROR: $option parameter needs a value after it\n" );
774
							$this->maybeHelp( true );
775
						}
776
						$this->setParam( $options, $option, $param );
777
					} else {
778
						$this->setParam( $options, $option, 1 );
779
					}
780
				}
781
			} else {
782
				$args[] = $arg;
783
			}
784
		}
785
786
		$this->mOptions = $options;
787
		$this->mArgs = $args;
788
		$this->loadSpecialVars();
789
		$this->mInputLoaded = true;
790
	}
791
792
	/**
793
	 * Helper function used solely by loadParamsAndArgs
794
	 * to prevent code duplication
795
	 *
796
	 * This sets the param in the options array based on
797
	 * whether or not it can be specified multiple times.
798
	 *
799
	 * @since 1.27
800
	 * @param array $options
801
	 * @param string $option
802
	 * @param mixed $value
803
	 */
804
	private function setParam( &$options, $option, $value ) {
805
		$this->orderedOptions[] = [ $option, $value ];
806
807
		if ( isset( $this->mParams[$option] ) ) {
808
			$multi = $this->mParams[$option]['multiOccurrence'];
809
		} else {
810
			$multi = false;
811
		}
812
		$exists = array_key_exists( $option, $options );
813
		if ( $multi && $exists ) {
814
			$options[$option][] = $value;
815
		} elseif ( $multi ) {
816
			$options[$option] = [ $value ];
817
		} elseif ( !$exists ) {
818
			$options[$option] = $value;
819
		} else {
820
			$this->error( "\nERROR: $option parameter given twice\n" );
821
			$this->maybeHelp( true );
822
		}
823
	}
824
825
	/**
826
	 * Process command line arguments
827
	 * $mOptions becomes an array with keys set to the option names
828
	 * $mArgs becomes a zero-based array containing the non-option arguments
829
	 *
830
	 * @param string $self The name of the script, if any
831
	 * @param array $opts An array of options, in form of key=>value
832
	 * @param array $args An array of command line arguments
833
	 */
834
	public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
835
		# If we were given opts or args, set those and return early
836
		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...
837
			$this->mSelf = $self;
838
			$this->mInputLoaded = true;
839
		}
840
		if ( $opts ) {
841
			$this->mOptions = $opts;
842
			$this->mInputLoaded = true;
843
		}
844
		if ( $args ) {
845
			$this->mArgs = $args;
846
			$this->mInputLoaded = true;
847
		}
848
849
		# If we've already loaded input (either by user values or from $argv)
850
		# skip on loading it again. The array_shift() will corrupt values if
851
		# it's run again and again
852
		if ( $this->mInputLoaded ) {
853
			$this->loadSpecialVars();
854
855
			return;
856
		}
857
858
		global $argv;
859
		$this->mSelf = $argv[0];
860
		$this->loadWithArgv( array_slice( $argv, 1 ) );
861
	}
862
863
	/**
864
	 * Run some validation checks on the params, etc
865
	 */
866
	protected function validateParamsAndArgs() {
867
		$die = false;
868
		# Check to make sure we've got all the required options
869
		foreach ( $this->mParams as $opt => $info ) {
870
			if ( $info['require'] && !$this->hasOption( $opt ) ) {
871
				$this->error( "Param $opt required!" );
872
				$die = true;
873
			}
874
		}
875
		# Check arg list too
876
		foreach ( $this->mArgList as $k => $info ) {
877
			if ( $info['require'] && !$this->hasArg( $k ) ) {
878
				$this->error( 'Argument <' . $info['name'] . '> required!' );
879
				$die = true;
880
			}
881
		}
882
883
		if ( $die ) {
884
			$this->maybeHelp( true );
885
		}
886
	}
887
888
	/**
889
	 * Handle the special variables that are global to all scripts
890
	 */
891
	protected function loadSpecialVars() {
892
		if ( $this->hasOption( 'dbuser' ) ) {
893
			$this->mDbUser = $this->getOption( 'dbuser' );
894
		}
895
		if ( $this->hasOption( 'dbpass' ) ) {
896
			$this->mDbPass = $this->getOption( 'dbpass' );
897
		}
898
		if ( $this->hasOption( 'quiet' ) ) {
899
			$this->mQuiet = true;
900
		}
901
		if ( $this->hasOption( 'batch-size' ) ) {
902
			$this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
903
		}
904
	}
905
906
	/**
907
	 * Maybe show the help.
908
	 * @param bool $force Whether to force the help to show, default false
909
	 */
910
	protected function maybeHelp( $force = false ) {
911
		if ( !$force && !$this->hasOption( 'help' ) ) {
912
			return;
913
		}
914
915
		$screenWidth = 80; // TODO: Calculate this!
916
		$tab = "    ";
917
		$descWidth = $screenWidth - ( 2 * strlen( $tab ) );
918
919
		ksort( $this->mParams );
920
		$this->mQuiet = false;
921
922
		// Description ...
923
		if ( $this->mDescription ) {
924
			$this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
925
		}
926
		$output = "\nUsage: php " . basename( $this->mSelf );
927
928
		// ... append parameters ...
929
		if ( $this->mParams ) {
930
			$output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
931
		}
932
933
		// ... and append arguments.
934
		if ( $this->mArgList ) {
935
			$output .= ' ';
936
			foreach ( $this->mArgList as $k => $arg ) {
937
				if ( $arg['require'] ) {
938
					$output .= '<' . $arg['name'] . '>';
939
				} else {
940
					$output .= '[' . $arg['name'] . ']';
941
				}
942
				if ( $k < count( $this->mArgList ) - 1 ) {
943
					$output .= ' ';
944
				}
945
			}
946
		}
947
		$this->output( "$output\n\n" );
948
949
		# TODO abstract some repetitive code below
950
951
		// Generic parameters
952
		$this->output( "Generic maintenance parameters:\n" );
953
		foreach ( $this->mGenericParameters as $par => $info ) {
954
			if ( $info['shortName'] !== false ) {
955
				$par .= " (-{$info['shortName']})";
956
			}
957
			$this->output(
958
				wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
959
					"\n$tab$tab" ) . "\n"
960
			);
961
		}
962
		$this->output( "\n" );
963
964
		$scriptDependantParams = $this->mDependantParameters;
965 View Code Duplication
		if ( count( $scriptDependantParams ) > 0 ) {
966
			$this->output( "Script dependant parameters:\n" );
967
			// Parameters description
968
			foreach ( $scriptDependantParams as $par => $info ) {
969
				if ( $info['shortName'] !== false ) {
970
					$par .= " (-{$info['shortName']})";
971
				}
972
				$this->output(
973
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
974
						"\n$tab$tab" ) . "\n"
975
				);
976
			}
977
			$this->output( "\n" );
978
		}
979
980
		// Script specific parameters not defined on construction by
981
		// Maintenance::addDefaultParams()
982
		$scriptSpecificParams = array_diff_key(
983
			# all script parameters:
984
			$this->mParams,
985
			# remove the Maintenance default parameters:
986
			$this->mGenericParameters,
987
			$this->mDependantParameters
988
		);
989 View Code Duplication
		if ( count( $scriptSpecificParams ) > 0 ) {
990
			$this->output( "Script specific parameters:\n" );
991
			// Parameters description
992
			foreach ( $scriptSpecificParams as $par => $info ) {
993
				if ( $info['shortName'] !== false ) {
994
					$par .= " (-{$info['shortName']})";
995
				}
996
				$this->output(
997
					wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
998
						"\n$tab$tab" ) . "\n"
999
				);
1000
			}
1001
			$this->output( "\n" );
1002
		}
1003
1004
		// Print arguments
1005
		if ( count( $this->mArgList ) > 0 ) {
1006
			$this->output( "Arguments:\n" );
1007
			// Arguments description
1008
			foreach ( $this->mArgList as $info ) {
1009
				$openChar = $info['require'] ? '<' : '[';
1010
				$closeChar = $info['require'] ? '>' : ']';
1011
				$this->output(
1012
					wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
1013
						$info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1014
				);
1015
			}
1016
			$this->output( "\n" );
1017
		}
1018
1019
		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...
1020
	}
1021
1022
	/**
1023
	 * Handle some last-minute setup here.
1024
	 */
1025
	public function finalSetup() {
1026
		global $wgCommandLineMode, $wgShowSQLErrors, $wgServer;
1027
		global $wgDBadminuser, $wgDBadminpassword;
1028
		global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
1029
1030
		# Turn off output buffering again, it might have been turned on in the settings files
1031
		if ( ob_get_level() ) {
1032
			ob_end_flush();
1033
		}
1034
		# Same with these
1035
		$wgCommandLineMode = true;
1036
1037
		# Override $wgServer
1038
		if ( $this->hasOption( 'server' ) ) {
1039
			$wgServer = $this->getOption( 'server', $wgServer );
1040
		}
1041
1042
		# If these were passed, use them
1043
		if ( $this->mDbUser ) {
1044
			$wgDBadminuser = $this->mDbUser;
1045
		}
1046
		if ( $this->mDbPass ) {
1047
			$wgDBadminpassword = $this->mDbPass;
1048
		}
1049
1050
		if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1051
			$wgDBuser = $wgDBadminuser;
1052
			$wgDBpassword = $wgDBadminpassword;
1053
1054
			if ( $wgDBservers ) {
1055
				/**
1056
				 * @var $wgDBservers array
1057
				 */
1058
				foreach ( $wgDBservers as $i => $server ) {
1059
					$wgDBservers[$i]['user'] = $wgDBuser;
1060
					$wgDBservers[$i]['password'] = $wgDBpassword;
1061
				}
1062
			}
1063
			if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1064
				$wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1065
				$wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1066
			}
1067
			LBFactory::destroyInstance();
1068
		}
1069
1070
		// Per-script profiling; useful for debugging
1071
		$this->activateProfiler();
1072
1073
		$this->afterFinalSetup();
1074
1075
		$wgShowSQLErrors = true;
1076
1077
		MediaWiki\suppressWarnings();
1078
		set_time_limit( 0 );
1079
		MediaWiki\restoreWarnings();
1080
1081
		$this->adjustMemoryLimit();
1082
	}
1083
1084
	/**
1085
	 * Execute a callback function at the end of initialisation
1086
	 */
1087
	protected function afterFinalSetup() {
1088
		if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1089
			call_user_func( MW_CMDLINE_CALLBACK );
1090
		}
1091
	}
1092
1093
	/**
1094
	 * Potentially debug globals. Originally a feature only
1095
	 * for refreshLinks
1096
	 */
1097
	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...
1098
		if ( $this->hasOption( 'globals' ) ) {
1099
			print_r( $GLOBALS );
1100
		}
1101
	}
1102
1103
	/**
1104
	 * Generic setup for most installs. Returns the location of LocalSettings
1105
	 * @return string
1106
	 */
1107
	public function loadSettings() {
1108
		global $wgCommandLineMode, $IP;
1109
1110
		if ( isset( $this->mOptions['conf'] ) ) {
1111
			$settingsFile = $this->mOptions['conf'];
1112
		} elseif ( defined( "MW_CONFIG_FILE" ) ) {
1113
			$settingsFile = MW_CONFIG_FILE;
1114
		} else {
1115
			$settingsFile = "$IP/LocalSettings.php";
1116
		}
1117
		if ( isset( $this->mOptions['wiki'] ) ) {
1118
			$bits = explode( '-', $this->mOptions['wiki'] );
1119
			if ( count( $bits ) == 1 ) {
1120
				$bits[] = '';
1121
			}
1122
			define( 'MW_DB', $bits[0] );
1123
			define( 'MW_PREFIX', $bits[1] );
1124
		}
1125
1126
		if ( !is_readable( $settingsFile ) ) {
1127
			$this->error( "A copy of your installation's LocalSettings.php\n" .
1128
				"must exist and be readable in the source directory.\n" .
1129
				"Use --conf to specify it.", true );
1130
		}
1131
		$wgCommandLineMode = true;
1132
1133
		return $settingsFile;
1134
	}
1135
1136
	/**
1137
	 * Support function for cleaning up redundant text records
1138
	 * @param bool $delete Whether or not to actually delete the records
1139
	 * @author Rob Church <[email protected]>
1140
	 */
1141
	public function purgeRedundantText( $delete = true ) {
1142
		# Data should come off the master, wrapped in a transaction
1143
		$dbw = $this->getDB( DB_MASTER );
1144
		$this->beginTransaction( $dbw, __METHOD__ );
1145
1146
		# Get "active" text records from the revisions table
1147
		$this->output( 'Searching for active text records in revisions table...' );
1148
		$res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1149
		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...
1150
			$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...
1151
		}
1152
		$this->output( "done.\n" );
1153
1154
		# Get "active" text records from the archive table
1155
		$this->output( 'Searching for active text records in archive table...' );
1156
		$res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1157
		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...
1158
			# old pre-MW 1.5 records can have null ar_text_id's.
1159
			if ( $row->ar_text_id !== null ) {
1160
				$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...
1161
			}
1162
		}
1163
		$this->output( "done.\n" );
1164
1165
		# Get the IDs of all text records not in these sets
1166
		$this->output( 'Searching for inactive text records...' );
1167
		$cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1168
		$res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1169
		$old = [];
1170
		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...
1171
			$old[] = $row->old_id;
1172
		}
1173
		$this->output( "done.\n" );
1174
1175
		# Inform the user of what we're going to do
1176
		$count = count( $old );
1177
		$this->output( "$count inactive items found.\n" );
1178
1179
		# Delete as appropriate
1180
		if ( $delete && $count ) {
1181
			$this->output( 'Deleting...' );
1182
			$dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1183
			$this->output( "done.\n" );
1184
		}
1185
1186
		# Done
1187
		$this->commitTransaction( $dbw, __METHOD__ );
1188
	}
1189
1190
	/**
1191
	 * Get the maintenance directory.
1192
	 * @return string
1193
	 */
1194
	protected function getDir() {
1195
		return __DIR__;
1196
	}
1197
1198
	/**
1199
	 * Returns a database to be used by current maintenance script. It can be set by setDB().
1200
	 * If not set, wfGetDB() will be used.
1201
	 * This function has the same parameters as wfGetDB()
1202
	 *
1203
	 * @param integer $db DB index (DB_SLAVE/DB_MASTER)
1204
	 * @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...
1205
	 * @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...
1206
	 * @return IDatabase
1207
	 */
1208
	protected function getDB( $db, $groups = [], $wiki = false ) {
1209
		if ( is_null( $this->mDb ) ) {
1210
			return wfGetDB( $db, $groups, $wiki );
1211
		} else {
1212
			return $this->mDb;
1213
		}
1214
	}
1215
1216
	/**
1217
	 * Sets database object to be returned by getDB().
1218
	 *
1219
	 * @param IDatabase $db Database object to be used
1220
	 */
1221
	public function setDB( IDatabase $db ) {
1222
		$this->mDb = $db;
1223
	}
1224
1225
	/**
1226
	 * Begin a transcation on a DB
1227
	 *
1228
	 * This method makes it clear that begin() is called from a maintenance script,
1229
	 * which has outermost scope. This is safe, unlike $dbw->begin() called in other places.
1230
	 *
1231
	 * @param IDatabase $dbw
1232
	 * @param string $fname Caller name
1233
	 * @since 1.27
1234
	 */
1235
	protected function beginTransaction( IDatabase $dbw, $fname ) {
1236
		$dbw->begin( $fname );
1237
	}
1238
1239
	/**
1240
	 * Commit the transcation on a DB handle and wait for slaves to catch up
1241
	 *
1242
	 * This method makes it clear that commit() is called from a maintenance script,
1243
	 * which has outermost scope. This is safe, unlike $dbw->commit() called in other places.
1244
	 *
1245
	 * @param IDatabase $dbw
1246
	 * @param string $fname Caller name
1247
	 * @return bool Whether the slave wait succeeded
1248
	 * @since 1.27
1249
	 */
1250
	protected function commitTransaction( IDatabase $dbw, $fname ) {
1251
		$dbw->commit( $fname );
1252
1253
		$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...
1254
		$this->lastSlaveWait = microtime( true );
1255
1256
		return $ok;
1257
	}
1258
1259
	/**
1260
	 * Rollback the transcation on a DB handle
1261
	 *
1262
	 * This method makes it clear that rollback() is called from a maintenance script,
1263
	 * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1264
	 *
1265
	 * @param IDatabase $dbw
1266
	 * @param string $fname Caller name
1267
	 * @since 1.27
1268
	 */
1269
	protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1270
		$dbw->rollback( $fname );
1271
	}
1272
1273
	/**
1274
	 * Lock the search index
1275
	 * @param DatabaseBase &$db
1276
	 */
1277
	private function lockSearchindex( $db ) {
1278
		$write = [ 'searchindex' ];
1279
		$read = [
1280
			'page',
1281
			'revision',
1282
			'text',
1283
			'interwiki',
1284
			'l10n_cache',
1285
			'user',
1286
			'page_restrictions'
1287
		];
1288
		$db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1289
	}
1290
1291
	/**
1292
	 * Unlock the tables
1293
	 * @param DatabaseBase &$db
1294
	 */
1295
	private function unlockSearchindex( $db ) {
1296
		$db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1297
	}
1298
1299
	/**
1300
	 * Unlock and lock again
1301
	 * Since the lock is low-priority, queued reads will be able to complete
1302
	 * @param DatabaseBase &$db
1303
	 */
1304
	private function relockSearchindex( $db ) {
1305
		$this->unlockSearchindex( $db );
1306
		$this->lockSearchindex( $db );
1307
	}
1308
1309
	/**
1310
	 * Perform a search index update with locking
1311
	 * @param int $maxLockTime The maximum time to keep the search index locked.
1312
	 * @param string $callback The function that will update the function.
1313
	 * @param DatabaseBase $dbw
1314
	 * @param array $results
1315
	 */
1316
	public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1317
		$lockTime = time();
1318
1319
		# Lock searchindex
1320
		if ( $maxLockTime ) {
1321
			$this->output( "   --- Waiting for lock ---" );
1322
			$this->lockSearchindex( $dbw );
1323
			$lockTime = time();
1324
			$this->output( "\n" );
1325
		}
1326
1327
		# Loop through the results and do a search update
1328
		foreach ( $results as $row ) {
1329
			# Allow reads to be processed
1330
			if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1331
				$this->output( "    --- Relocking ---" );
1332
				$this->relockSearchindex( $dbw );
1333
				$lockTime = time();
1334
				$this->output( "\n" );
1335
			}
1336
			call_user_func( $callback, $dbw, $row );
1337
		}
1338
1339
		# Unlock searchindex
1340
		if ( $maxLockTime ) {
1341
			$this->output( "    --- Unlocking --" );
1342
			$this->unlockSearchindex( $dbw );
1343
			$this->output( "\n" );
1344
		}
1345
	}
1346
1347
	/**
1348
	 * Update the searchindex table for a given pageid
1349
	 * @param DatabaseBase $dbw A database write handle
1350
	 * @param int $pageId The page ID to update.
1351
	 * @return null|string
1352
	 */
1353
	public function updateSearchIndexForPage( $dbw, $pageId ) {
1354
		// Get current revision
1355
		$rev = Revision::loadFromPageId( $dbw, $pageId );
1356
		$title = null;
1357
		if ( $rev ) {
1358
			$titleObj = $rev->getTitle();
1359
			$title = $titleObj->getPrefixedDBkey();
1360
			$this->output( "$title..." );
1361
			# Update searchindex
1362
			$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...
1363
			$u->doUpdate();
1364
			$this->output( "\n" );
1365
		}
1366
1367
		return $title;
1368
	}
1369
1370
	/**
1371
	 * Wrapper for posix_isatty()
1372
	 * We default as considering stdin a tty (for nice readline methods)
1373
	 * but treating stout as not a tty to avoid color codes
1374
	 *
1375
	 * @param mixed $fd File descriptor
1376
	 * @return bool
1377
	 */
1378
	public static function posix_isatty( $fd ) {
1379
		if ( !function_exists( 'posix_isatty' ) ) {
1380
			return !$fd;
1381
		} else {
1382
			return posix_isatty( $fd );
1383
		}
1384
	}
1385
1386
	/**
1387
	 * Prompt the console for input
1388
	 * @param string $prompt What to begin the line with, like '> '
1389
	 * @return string Response
1390
	 */
1391
	public static function readconsole( $prompt = '> ' ) {
1392
		static $isatty = null;
1393
		if ( is_null( $isatty ) ) {
1394
			$isatty = self::posix_isatty( 0 /*STDIN*/ );
1395
		}
1396
1397
		if ( $isatty && function_exists( 'readline' ) ) {
1398
			$resp = readline( $prompt );
1399
			if ( $resp === null ) {
1400
				// Workaround for https://github.com/facebook/hhvm/issues/4776
1401
				return false;
1402
			} else {
1403
				return $resp;
1404
			}
1405
		} else {
1406
			if ( $isatty ) {
1407
				$st = self::readlineEmulation( $prompt );
1408
			} else {
1409
				if ( feof( STDIN ) ) {
1410
					$st = false;
1411
				} else {
1412
					$st = fgets( STDIN, 1024 );
1413
				}
1414
			}
1415
			if ( $st === false ) {
1416
				return false;
1417
			}
1418
			$resp = trim( $st );
1419
1420
			return $resp;
1421
		}
1422
	}
1423
1424
	/**
1425
	 * Emulate readline()
1426
	 * @param string $prompt What to begin the line with, like '> '
1427
	 * @return string
1428
	 */
1429
	private static function readlineEmulation( $prompt ) {
1430
		$bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1431
		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...
1432
			$retval = false;
1433
			$encPrompt = wfEscapeShellArg( $prompt );
1434
			$command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1435
			$encCommand = wfEscapeShellArg( $command );
1436
			$line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1437
1438
			if ( $retval == 0 ) {
1439
				return $line;
1440
			} 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...
1441
				// Couldn't execute bash even though we thought we saw it.
1442
				// Shell probably spit out an error message, sorry :(
1443
				// Fall through to fgets()...
1444
			} else {
1445
				// EOF/ctrl+D
1446
				return false;
1447
			}
1448
		}
1449
1450
		// Fallback... we'll have no editing controls, EWWW
1451
		if ( feof( STDIN ) ) {
1452
			return false;
1453
		}
1454
		print $prompt;
1455
1456
		return fgets( STDIN, 1024 );
1457
	}
1458
}
1459
1460
/**
1461
 * Fake maintenance wrapper, mostly used for the web installer/updater
1462
 */
1463
class FakeMaintenance extends Maintenance {
1464
	protected $mSelf = "FakeMaintenanceScript";
1465
1466
	public function execute() {
1467
		return;
1468
	}
1469
}
1470
1471
/**
1472
 * Class for scripts that perform database maintenance and want to log the
1473
 * update in `updatelog` so we can later skip it
1474
 */
1475
abstract class LoggedUpdateMaintenance extends Maintenance {
1476
	public function __construct() {
1477
		parent::__construct();
1478
		$this->addOption( 'force', 'Run the update even if it was completed already' );
1479
		$this->setBatchSize( 200 );
1480
	}
1481
1482
	public function execute() {
1483
		$db = $this->getDB( DB_MASTER );
1484
		$key = $this->getUpdateKey();
1485
1486
		if ( !$this->hasOption( 'force' )
1487
			&& $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1488
		) {
1489
			$this->output( "..." . $this->updateSkippedMessage() . "\n" );
1490
1491
			return true;
1492
		}
1493
1494
		if ( !$this->doDBUpdates() ) {
1495
			return false;
1496
		}
1497
1498 View Code Duplication
		if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1499
			return true;
1500
		} else {
1501
			$this->output( $this->updatelogFailedMessage() . "\n" );
1502
1503
			return false;
1504
		}
1505
	}
1506
1507
	/**
1508
	 * Message to show that the update was done already and was just skipped
1509
	 * @return string
1510
	 */
1511
	protected function updateSkippedMessage() {
1512
		$key = $this->getUpdateKey();
1513
1514
		return "Update '{$key}' already logged as completed.";
1515
	}
1516
1517
	/**
1518
	 * Message to show that the update log was unable to log the completion of this update
1519
	 * @return string
1520
	 */
1521
	protected function updatelogFailedMessage() {
1522
		$key = $this->getUpdateKey();
1523
1524
		return "Unable to log update '{$key}' as completed.";
1525
	}
1526
1527
	/**
1528
	 * Do the actual work. All child classes will need to implement this.
1529
	 * Return true to log the update as done or false (usually on failure).
1530
	 * @return bool
1531
	 */
1532
	abstract protected function doDBUpdates();
1533
1534
	/**
1535
	 * Get the update key name to go in the update log table
1536
	 * @return string
1537
	 */
1538
	abstract protected function getUpdateKey();
1539
}
1540