1
|
|
|
<?php |
|
|
|
|
2
|
|
|
/** |
3
|
|
|
* This program is free software; you can redistribute it and/or modify |
4
|
|
|
* it under the terms of the GNU General Public License as published by |
5
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
6
|
|
|
* (at your option) any later version. |
7
|
|
|
* |
8
|
|
|
* This program is distributed in the hope that it will be useful, |
9
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
10
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11
|
|
|
* GNU General Public License for more details. |
12
|
|
|
* |
13
|
|
|
* You should have received a copy of the GNU General Public License along |
14
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc., |
15
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16
|
|
|
* http://www.gnu.org/copyleft/gpl.html |
17
|
|
|
* |
18
|
|
|
* @file |
19
|
|
|
* @ingroup Maintenance |
20
|
|
|
* @defgroup Maintenance Maintenance |
21
|
|
|
*/ |
22
|
|
|
|
23
|
|
|
// Bail on old versions of PHP, or if composer has not been run yet to install |
24
|
|
|
// dependencies. |
25
|
|
|
require_once __DIR__ . '/../includes/PHPVersionCheck.php'; |
26
|
|
|
wfEntryPointCheck( 'cli' ); |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @defgroup MaintenanceArchive Maintenance archives |
30
|
|
|
* @ingroup Maintenance |
31
|
|
|
*/ |
32
|
|
|
|
33
|
|
|
// Define this so scripts can easily find doMaintenance.php |
34
|
|
|
define( 'RUN_MAINTENANCE_IF_MAIN', __DIR__ . '/doMaintenance.php' ); |
35
|
|
|
define( 'DO_MAINTENANCE', RUN_MAINTENANCE_IF_MAIN ); // original name, harmless |
36
|
|
|
|
37
|
|
|
$maintClass = false; |
38
|
|
|
|
39
|
|
|
use MediaWiki\Logger\LoggerFactory; |
40
|
|
|
use MediaWiki\MediaWikiServices; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Abstract maintenance class for quickly writing and churning out |
44
|
|
|
* maintenance scripts with minimal effort. All that _must_ be defined |
45
|
|
|
* is the execute() method. See docs/maintenance.txt for more info |
46
|
|
|
* and a quick demo of how to use it. |
47
|
|
|
* |
48
|
|
|
* @author Chad Horohoe <[email protected]> |
49
|
|
|
* @since 1.16 |
50
|
|
|
* @ingroup Maintenance |
51
|
|
|
*/ |
52
|
|
|
abstract class Maintenance { |
53
|
|
|
/** |
54
|
|
|
* Constants for DB access type |
55
|
|
|
* @see Maintenance::getDbType() |
56
|
|
|
*/ |
57
|
|
|
const DB_NONE = 0; |
58
|
|
|
const DB_STD = 1; |
59
|
|
|
const DB_ADMIN = 2; |
60
|
|
|
|
61
|
|
|
// Const for getStdin() |
62
|
|
|
const STDIN_ALL = 'all'; |
63
|
|
|
|
64
|
|
|
// This is the desired params |
65
|
|
|
protected $mParams = []; |
66
|
|
|
|
67
|
|
|
// Array of mapping short parameters to long ones |
68
|
|
|
protected $mShortParamsMap = []; |
69
|
|
|
|
70
|
|
|
// Array of desired args |
71
|
|
|
protected $mArgList = []; |
72
|
|
|
|
73
|
|
|
// This is the list of options that were actually passed |
74
|
|
|
protected $mOptions = []; |
75
|
|
|
|
76
|
|
|
// This is the list of arguments that were actually passed |
77
|
|
|
protected $mArgs = []; |
78
|
|
|
|
79
|
|
|
// Name of the script currently running |
80
|
|
|
protected $mSelf; |
81
|
|
|
|
82
|
|
|
// Special vars for params that are always used |
83
|
|
|
protected $mQuiet = false; |
84
|
|
|
protected $mDbUser, $mDbPass; |
|
|
|
|
85
|
|
|
|
86
|
|
|
// A description of the script, children should change this via addDescription() |
87
|
|
|
protected $mDescription = ''; |
88
|
|
|
|
89
|
|
|
// Have we already loaded our user input? |
90
|
|
|
protected $mInputLoaded = false; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Batch size. If a script supports this, they should set |
94
|
|
|
* a default with setBatchSize() |
95
|
|
|
* |
96
|
|
|
* @var int |
97
|
|
|
*/ |
98
|
|
|
protected $mBatchSize = null; |
99
|
|
|
|
100
|
|
|
// Generic options added by addDefaultParams() |
101
|
|
|
private $mGenericParameters = []; |
102
|
|
|
// Generic options which might or not be supported by the script |
103
|
|
|
private $mDependantParameters = []; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Used by getDB() / setDB() |
107
|
|
|
* @var Database |
108
|
|
|
*/ |
109
|
|
|
private $mDb = null; |
110
|
|
|
|
111
|
|
|
/** @var float UNIX timestamp */ |
112
|
|
|
private $lastReplicationWait = 0.0; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Used when creating separate schema files. |
116
|
|
|
* @var resource |
117
|
|
|
*/ |
118
|
|
|
public $fileHandle; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Accessible via getConfig() |
122
|
|
|
* |
123
|
|
|
* @var Config |
124
|
|
|
*/ |
125
|
|
|
private $config; |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @see Maintenance::requireExtension |
129
|
|
|
* @var array |
130
|
|
|
*/ |
131
|
|
|
private $requiredExtensions = []; |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Used to read the options in the order they were passed. |
135
|
|
|
* Useful for option chaining (Ex. dumpBackup.php). It will |
136
|
|
|
* be an empty array if the options are passed in through |
137
|
|
|
* loadParamsAndArgs( $self, $opts, $args ). |
138
|
|
|
* |
139
|
|
|
* This is an array of arrays where |
140
|
|
|
* 0 => the option and 1 => parameter value. |
141
|
|
|
* |
142
|
|
|
* @var array |
143
|
|
|
*/ |
144
|
|
|
public $orderedOptions = []; |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Default constructor. Children should call this *first* if implementing |
148
|
|
|
* their own constructors |
149
|
|
|
*/ |
150
|
|
|
public function __construct() { |
151
|
|
|
// Setup $IP, using MW_INSTALL_PATH if it exists |
152
|
|
|
global $IP; |
153
|
|
|
$IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== '' |
154
|
|
|
? getenv( 'MW_INSTALL_PATH' ) |
155
|
|
|
: realpath( __DIR__ . '/..' ); |
156
|
|
|
|
157
|
|
|
$this->addDefaultParams(); |
158
|
|
|
register_shutdown_function( [ $this, 'outputChanneled' ], false ); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Should we execute the maintenance script, or just allow it to be included |
163
|
|
|
* as a standalone class? It checks that the call stack only includes this |
164
|
|
|
* function and "requires" (meaning was called from the file scope) |
165
|
|
|
* |
166
|
|
|
* @return bool |
167
|
|
|
*/ |
168
|
|
|
public static function shouldExecute() { |
169
|
|
|
global $wgCommandLineMode; |
170
|
|
|
|
171
|
|
|
if ( !function_exists( 'debug_backtrace' ) ) { |
172
|
|
|
// If someone has a better idea... |
173
|
|
|
return $wgCommandLineMode; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$bt = debug_backtrace(); |
177
|
|
|
$count = count( $bt ); |
178
|
|
|
if ( $count < 2 ) { |
179
|
|
|
return false; // sanity |
180
|
|
|
} |
181
|
|
|
if ( $bt[0]['class'] !== 'Maintenance' || $bt[0]['function'] !== 'shouldExecute' ) { |
182
|
|
|
return false; // last call should be to this function |
183
|
|
|
} |
184
|
|
|
$includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ]; |
185
|
|
|
for ( $i = 1; $i < $count; $i++ ) { |
186
|
|
|
if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) { |
187
|
|
|
return false; // previous calls should all be "requires" |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
return true; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Do the actual work. All child classes will need to implement this |
196
|
|
|
*/ |
197
|
|
|
abstract public function execute(); |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Add a parameter to the script. Will be displayed on --help |
201
|
|
|
* with the associated description |
202
|
|
|
* |
203
|
|
|
* @param string $name The name of the param (help, version, etc) |
204
|
|
|
* @param string $description The description of the param to show on --help |
205
|
|
|
* @param bool $required Is the param required? |
206
|
|
|
* @param bool $withArg Is an argument required with this option? |
207
|
|
|
* @param string $shortName Character to use as short name |
208
|
|
|
* @param bool $multiOccurrence Can this option be passed multiple times? |
209
|
|
|
*/ |
210
|
|
|
protected function addOption( $name, $description, $required = false, |
211
|
|
|
$withArg = false, $shortName = false, $multiOccurrence = false |
212
|
|
|
) { |
213
|
|
|
$this->mParams[$name] = [ |
214
|
|
|
'desc' => $description, |
215
|
|
|
'require' => $required, |
216
|
|
|
'withArg' => $withArg, |
217
|
|
|
'shortName' => $shortName, |
218
|
|
|
'multiOccurrence' => $multiOccurrence |
219
|
|
|
]; |
220
|
|
|
|
221
|
|
|
if ( $shortName !== false ) { |
222
|
|
|
$this->mShortParamsMap[$shortName] = $name; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Checks to see if a particular param exists. |
228
|
|
|
* @param string $name The name of the param |
229
|
|
|
* @return bool |
230
|
|
|
*/ |
231
|
|
|
protected function hasOption( $name ) { |
232
|
|
|
return isset( $this->mOptions[$name] ); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Get an option, or return the default. |
237
|
|
|
* |
238
|
|
|
* If the option was added to support multiple occurrences, |
239
|
|
|
* this will return an array. |
240
|
|
|
* |
241
|
|
|
* @param string $name The name of the param |
242
|
|
|
* @param mixed $default Anything you want, default null |
243
|
|
|
* @return mixed |
244
|
|
|
*/ |
245
|
|
|
protected function getOption( $name, $default = null ) { |
246
|
|
|
if ( $this->hasOption( $name ) ) { |
247
|
|
|
return $this->mOptions[$name]; |
248
|
|
|
} else { |
249
|
|
|
// Set it so we don't have to provide the default again |
250
|
|
|
$this->mOptions[$name] = $default; |
251
|
|
|
|
252
|
|
|
return $this->mOptions[$name]; |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Add some args that are needed |
258
|
|
|
* @param string $arg Name of the arg, like 'start' |
259
|
|
|
* @param string $description Short description of the arg |
260
|
|
|
* @param bool $required Is this required? |
261
|
|
|
*/ |
262
|
|
|
protected function addArg( $arg, $description, $required = true ) { |
263
|
|
|
$this->mArgList[] = [ |
264
|
|
|
'name' => $arg, |
265
|
|
|
'desc' => $description, |
266
|
|
|
'require' => $required |
267
|
|
|
]; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Remove an option. Useful for removing options that won't be used in your script. |
272
|
|
|
* @param string $name The option to remove. |
273
|
|
|
*/ |
274
|
|
|
protected function deleteOption( $name ) { |
275
|
|
|
unset( $this->mParams[$name] ); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Set the description text. |
280
|
|
|
* @param string $text The text of the description |
281
|
|
|
*/ |
282
|
|
|
protected function addDescription( $text ) { |
283
|
|
|
$this->mDescription = $text; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Does a given argument exist? |
288
|
|
|
* @param int $argId The integer value (from zero) for the arg |
289
|
|
|
* @return bool |
290
|
|
|
*/ |
291
|
|
|
protected function hasArg( $argId = 0 ) { |
292
|
|
|
return isset( $this->mArgs[$argId] ); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Get an argument. |
297
|
|
|
* @param int $argId The integer value (from zero) for the arg |
298
|
|
|
* @param mixed $default The default if it doesn't exist |
299
|
|
|
* @return mixed |
300
|
|
|
*/ |
301
|
|
|
protected function getArg( $argId = 0, $default = null ) { |
302
|
|
|
return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Set the batch size. |
307
|
|
|
* @param int $s The number of operations to do in a batch |
308
|
|
|
*/ |
309
|
|
|
protected function setBatchSize( $s = 0 ) { |
310
|
|
|
$this->mBatchSize = $s; |
311
|
|
|
|
312
|
|
|
// If we support $mBatchSize, show the option. |
313
|
|
|
// Used to be in addDefaultParams, but in order for that to |
314
|
|
|
// work, subclasses would have to call this function in the constructor |
315
|
|
|
// before they called parent::__construct which is just weird |
316
|
|
|
// (and really wasn't done). |
317
|
|
|
if ( $this->mBatchSize ) { |
318
|
|
|
$this->addOption( 'batch-size', 'Run this many operations ' . |
319
|
|
|
'per batch, default: ' . $this->mBatchSize, false, true ); |
320
|
|
|
if ( isset( $this->mParams['batch-size'] ) ) { |
321
|
|
|
// This seems a little ugly... |
322
|
|
|
$this->mDependantParameters['batch-size'] = $this->mParams['batch-size']; |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Get the script's name |
329
|
|
|
* @return string |
330
|
|
|
*/ |
331
|
|
|
public function getName() { |
332
|
|
|
return $this->mSelf; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Return input from stdin. |
337
|
|
|
* @param int $len The number of bytes to read. If null, just return the handle. |
338
|
|
|
* Maintenance::STDIN_ALL returns the full length |
339
|
|
|
* @return mixed |
340
|
|
|
*/ |
341
|
|
|
protected function getStdin( $len = null ) { |
342
|
|
|
if ( $len == Maintenance::STDIN_ALL ) { |
343
|
|
|
return file_get_contents( 'php://stdin' ); |
344
|
|
|
} |
345
|
|
|
$f = fopen( 'php://stdin', 'rt' ); |
346
|
|
|
if ( !$len ) { |
|
|
|
|
347
|
|
|
return $f; |
348
|
|
|
} |
349
|
|
|
$input = fgets( $f, $len ); |
350
|
|
|
fclose( $f ); |
351
|
|
|
|
352
|
|
|
return rtrim( $input ); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @return bool |
357
|
|
|
*/ |
358
|
|
|
public function isQuiet() { |
359
|
|
|
return $this->mQuiet; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Throw some output to the user. Scripts can call this with no fears, |
364
|
|
|
* as we handle all --quiet stuff here |
365
|
|
|
* @param string $out The text to show to the user |
366
|
|
|
* @param mixed $channel Unique identifier for the channel. See function outputChanneled. |
367
|
|
|
*/ |
368
|
|
|
protected function output( $out, $channel = null ) { |
369
|
|
|
if ( $this->mQuiet ) { |
370
|
|
|
return; |
371
|
|
|
} |
372
|
|
|
if ( $channel === null ) { |
373
|
|
|
$this->cleanupChanneled(); |
374
|
|
|
print $out; |
375
|
|
|
} else { |
376
|
|
|
$out = preg_replace( '/\n\z/', '', $out ); |
377
|
|
|
$this->outputChanneled( $out, $channel ); |
378
|
|
|
} |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Throw an error to the user. Doesn't respect --quiet, so don't use |
383
|
|
|
* this for non-error output |
384
|
|
|
* @param string $err The error to display |
385
|
|
|
* @param int $die If > 0, go ahead and die out using this int as the code |
386
|
|
|
*/ |
387
|
|
|
protected function error( $err, $die = 0 ) { |
388
|
|
|
$this->outputChanneled( false ); |
389
|
|
|
if ( PHP_SAPI == 'cli' ) { |
390
|
|
|
fwrite( STDERR, $err . "\n" ); |
391
|
|
|
} else { |
392
|
|
|
print $err; |
393
|
|
|
} |
394
|
|
|
$die = intval( $die ); |
395
|
|
|
if ( $die > 0 ) { |
396
|
|
|
die( $die ); |
|
|
|
|
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
private $atLineStart = true; |
401
|
|
|
private $lastChannel = null; |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Clean up channeled output. Output a newline if necessary. |
405
|
|
|
*/ |
406
|
|
|
public function cleanupChanneled() { |
407
|
|
|
if ( !$this->atLineStart ) { |
408
|
|
|
print "\n"; |
409
|
|
|
$this->atLineStart = true; |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* Message outputter with channeled message support. Messages on the |
415
|
|
|
* same channel are concatenated, but any intervening messages in another |
416
|
|
|
* channel start a new line. |
417
|
|
|
* @param string $msg The message without trailing newline |
418
|
|
|
* @param string $channel Channel identifier or null for no |
419
|
|
|
* channel. Channel comparison uses ===. |
420
|
|
|
*/ |
421
|
|
|
public function outputChanneled( $msg, $channel = null ) { |
422
|
|
|
if ( $msg === false ) { |
423
|
|
|
$this->cleanupChanneled(); |
424
|
|
|
|
425
|
|
|
return; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
// End the current line if necessary |
429
|
|
|
if ( !$this->atLineStart && $channel !== $this->lastChannel ) { |
430
|
|
|
print "\n"; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
print $msg; |
434
|
|
|
|
435
|
|
|
$this->atLineStart = false; |
436
|
|
|
if ( $channel === null ) { |
437
|
|
|
// For unchanneled messages, output trailing newline immediately |
438
|
|
|
print "\n"; |
439
|
|
|
$this->atLineStart = true; |
440
|
|
|
} |
441
|
|
|
$this->lastChannel = $channel; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* Does the script need different DB access? By default, we give Maintenance |
446
|
|
|
* scripts normal rights to the DB. Sometimes, a script needs admin rights |
447
|
|
|
* access for a reason and sometimes they want no access. Subclasses should |
448
|
|
|
* override and return one of the following values, as needed: |
449
|
|
|
* Maintenance::DB_NONE - For no DB access at all |
450
|
|
|
* Maintenance::DB_STD - For normal DB access, default |
451
|
|
|
* Maintenance::DB_ADMIN - For admin DB access |
452
|
|
|
* @return int |
453
|
|
|
*/ |
454
|
|
|
public function getDbType() { |
455
|
|
|
return Maintenance::DB_STD; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* Add the default parameters to the scripts |
460
|
|
|
*/ |
461
|
|
|
protected function addDefaultParams() { |
462
|
|
|
|
463
|
|
|
# Generic (non script dependant) options: |
464
|
|
|
|
465
|
|
|
$this->addOption( 'help', 'Display this help message', false, false, 'h' ); |
466
|
|
|
$this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' ); |
467
|
|
|
$this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true ); |
468
|
|
|
$this->addOption( 'wiki', 'For specifying the wiki ID', false, true ); |
469
|
|
|
$this->addOption( 'globals', 'Output globals at the end of processing for debugging' ); |
470
|
|
|
$this->addOption( |
471
|
|
|
'memory-limit', |
472
|
|
|
'Set a specific memory limit for the script, ' |
473
|
|
|
. '"max" for no limit or "default" to avoid changing it' |
474
|
|
|
); |
475
|
|
|
$this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " . |
476
|
|
|
"http://en.wikipedia.org. This is sometimes necessary because " . |
477
|
|
|
"server name detection may fail in command line scripts.", false, true ); |
478
|
|
|
$this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true ); |
479
|
|
|
|
480
|
|
|
# Save generic options to display them separately in help |
481
|
|
|
$this->mGenericParameters = $this->mParams; |
482
|
|
|
|
483
|
|
|
# Script dependant options: |
484
|
|
|
|
485
|
|
|
// If we support a DB, show the options |
486
|
|
|
if ( $this->getDbType() > 0 ) { |
487
|
|
|
$this->addOption( 'dbuser', 'The DB user to use for this script', false, true ); |
488
|
|
|
$this->addOption( 'dbpass', 'The password to use for this script', false, true ); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
# Save additional script dependant options to display |
492
|
|
|
# them separately in help |
493
|
|
|
$this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters ); |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* @since 1.24 |
498
|
|
|
* @return Config |
499
|
|
|
*/ |
500
|
|
View Code Duplication |
public function getConfig() { |
501
|
|
|
if ( $this->config === null ) { |
502
|
|
|
$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
return $this->config; |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
/** |
509
|
|
|
* @since 1.24 |
510
|
|
|
* @param Config $config |
511
|
|
|
*/ |
512
|
|
|
public function setConfig( Config $config ) { |
513
|
|
|
$this->config = $config; |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Indicate that the specified extension must be |
518
|
|
|
* loaded before the script can run. |
519
|
|
|
* |
520
|
|
|
* This *must* be called in the constructor. |
521
|
|
|
* |
522
|
|
|
* @since 1.28 |
523
|
|
|
* @param string $name |
524
|
|
|
*/ |
525
|
|
|
protected function requireExtension( $name ) { |
526
|
|
|
$this->requiredExtensions[] = $name; |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
/** |
530
|
|
|
* Verify that the required extensions are installed |
531
|
|
|
* |
532
|
|
|
* @since 1.28 |
533
|
|
|
*/ |
534
|
|
|
public function checkRequiredExtensions() { |
535
|
|
|
$registry = ExtensionRegistry::getInstance(); |
536
|
|
|
$missing = []; |
537
|
|
|
foreach ( $this->requiredExtensions as $name ) { |
538
|
|
|
if ( !$registry->isLoaded( $name ) ) { |
539
|
|
|
$missing[] = $name; |
540
|
|
|
} |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
if ( $missing ) { |
544
|
|
|
$joined = implode( ', ', $missing ); |
545
|
|
|
$msg = "The following extensions are required to be installed " |
546
|
|
|
. "for this script to run: $joined. Please enable them and then try again."; |
547
|
|
|
$this->error( $msg, 1 ); |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* Set triggers like when to try to run deferred updates |
554
|
|
|
* @since 1.28 |
555
|
|
|
*/ |
556
|
|
|
public function setAgentAndTriggers() { |
557
|
|
|
if ( function_exists( 'posix_getpwuid' ) ) { |
558
|
|
|
$agent = posix_getpwuid( posix_geteuid() )['name']; |
559
|
|
|
} else { |
560
|
|
|
$agent = 'sysadmin'; |
561
|
|
|
} |
562
|
|
|
$agent .= '@' . wfHostname(); |
563
|
|
|
|
564
|
|
|
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
565
|
|
|
// Add a comment for easy SHOW PROCESSLIST interpretation |
566
|
|
|
$lbFactory->setAgentName( |
567
|
|
|
mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent |
568
|
|
|
); |
569
|
|
|
self::setLBFactoryTriggers( $lbFactory ); |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
/** |
573
|
|
|
* @param LBFactory $LBFactory |
574
|
|
|
* @since 1.28 |
575
|
|
|
*/ |
576
|
|
|
public static function setLBFactoryTriggers( LBFactory $LBFactory ) { |
577
|
|
|
// Hook into period lag checks which often happen in long-running scripts |
578
|
|
|
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
579
|
|
|
$lbFactory->setWaitForReplicationListener( |
580
|
|
|
__METHOD__, |
581
|
|
|
function () { |
582
|
|
|
global $wgCommandLineMode; |
583
|
|
|
// Check config in case of JobRunner and unit tests |
584
|
|
|
if ( $wgCommandLineMode ) { |
585
|
|
|
DeferredUpdates::tryOpportunisticExecute( 'run' ); |
586
|
|
|
} |
587
|
|
|
} |
588
|
|
|
); |
589
|
|
|
// Check for other windows to run them. A script may read or do a few writes |
590
|
|
|
// to the master but mostly be writing to something else, like a file store. |
591
|
|
|
$lbFactory->getMainLB()->setTransactionListener( |
592
|
|
|
__METHOD__, |
593
|
|
|
function ( $trigger ) { |
594
|
|
|
global $wgCommandLineMode; |
595
|
|
|
// Check config in case of JobRunner and unit tests |
596
|
|
|
if ( $wgCommandLineMode && $trigger === IDatabase::TRIGGER_COMMIT ) { |
597
|
|
|
DeferredUpdates::tryOpportunisticExecute( 'run' ); |
598
|
|
|
} |
599
|
|
|
} |
600
|
|
|
); |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
/** |
604
|
|
|
* Run a child maintenance script. Pass all of the current arguments |
605
|
|
|
* to it. |
606
|
|
|
* @param string $maintClass A name of a child maintenance class |
607
|
|
|
* @param string $classFile Full path of where the child is |
608
|
|
|
* @return Maintenance |
609
|
|
|
*/ |
610
|
|
|
public function runChild( $maintClass, $classFile = null ) { |
611
|
|
|
// Make sure the class is loaded first |
612
|
|
|
if ( !class_exists( $maintClass ) ) { |
613
|
|
|
if ( $classFile ) { |
|
|
|
|
614
|
|
|
require_once $classFile; |
615
|
|
|
} |
616
|
|
|
if ( !class_exists( $maintClass ) ) { |
617
|
|
|
$this->error( "Cannot spawn child: $maintClass" ); |
618
|
|
|
} |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* @var $child Maintenance |
623
|
|
|
*/ |
624
|
|
|
$child = new $maintClass(); |
625
|
|
|
$child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs ); |
626
|
|
|
if ( !is_null( $this->mDb ) ) { |
627
|
|
|
$child->setDB( $this->mDb ); |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
return $child; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* Do some sanity checking and basic setup |
635
|
|
|
*/ |
636
|
|
|
public function setup() { |
|
|
|
|
637
|
|
|
global $IP, $wgCommandLineMode, $wgRequestTime; |
638
|
|
|
|
639
|
|
|
# Abort if called from a web server |
640
|
|
|
if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) { |
641
|
|
|
$this->error( 'This script must be run from the command line', true ); |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
if ( $IP === null ) { |
645
|
|
|
$this->error( "\$IP not set, aborting!\n" . |
646
|
|
|
'(Did you forget to call parent::__construct() in your maintenance script?)', 1 ); |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
# Make sure we can handle script parameters |
650
|
|
|
if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) { |
651
|
|
|
$this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true ); |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
// Send PHP warnings and errors to stderr instead of stdout. |
655
|
|
|
// This aids in diagnosing problems, while keeping messages |
656
|
|
|
// out of redirected output. |
657
|
|
|
if ( ini_get( 'display_errors' ) ) { |
658
|
|
|
ini_set( 'display_errors', 'stderr' ); |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
$this->loadParamsAndArgs(); |
662
|
|
|
$this->maybeHelp(); |
663
|
|
|
|
664
|
|
|
# Set the memory limit |
665
|
|
|
# Note we need to set it again later in cache LocalSettings changed it |
666
|
|
|
$this->adjustMemoryLimit(); |
667
|
|
|
|
668
|
|
|
# Set max execution time to 0 (no limit). PHP.net says that |
669
|
|
|
# "When running PHP from the command line the default setting is 0." |
670
|
|
|
# But sometimes this doesn't seem to be the case. |
671
|
|
|
ini_set( 'max_execution_time', 0 ); |
672
|
|
|
|
673
|
|
|
$wgRequestTime = microtime( true ); |
674
|
|
|
|
675
|
|
|
# Define us as being in MediaWiki |
676
|
|
|
define( 'MEDIAWIKI', true ); |
677
|
|
|
|
678
|
|
|
$wgCommandLineMode = true; |
679
|
|
|
|
680
|
|
|
# Turn off output buffering if it's on |
681
|
|
|
while ( ob_get_level() > 0 ) { |
682
|
|
|
ob_end_flush(); |
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
$this->validateParamsAndArgs(); |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
/** |
689
|
|
|
* Normally we disable the memory_limit when running admin scripts. |
690
|
|
|
* Some scripts may wish to actually set a limit, however, to avoid |
691
|
|
|
* blowing up unexpectedly. We also support a --memory-limit option, |
692
|
|
|
* to allow sysadmins to explicitly set one if they'd prefer to override |
693
|
|
|
* defaults (or for people using Suhosin which yells at you for trying |
694
|
|
|
* to disable the limits) |
695
|
|
|
* @return string |
696
|
|
|
*/ |
697
|
|
|
public function memoryLimit() { |
698
|
|
|
$limit = $this->getOption( 'memory-limit', 'max' ); |
699
|
|
|
$limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood |
700
|
|
|
return $limit; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
/** |
704
|
|
|
* Adjusts PHP's memory limit to better suit our needs, if needed. |
705
|
|
|
*/ |
706
|
|
|
protected function adjustMemoryLimit() { |
707
|
|
|
$limit = $this->memoryLimit(); |
708
|
|
|
if ( $limit == 'max' ) { |
709
|
|
|
$limit = -1; // no memory limit |
710
|
|
|
} |
711
|
|
|
if ( $limit != 'default' ) { |
712
|
|
|
ini_set( 'memory_limit', $limit ); |
713
|
|
|
} |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
/** |
717
|
|
|
* Activate the profiler (assuming $wgProfiler is set) |
718
|
|
|
*/ |
719
|
|
|
protected function activateProfiler() { |
720
|
|
|
global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits; |
721
|
|
|
|
722
|
|
|
$output = $this->getOption( 'profiler' ); |
723
|
|
|
if ( !$output ) { |
724
|
|
|
return; |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) { |
728
|
|
|
$class = $wgProfiler['class']; |
729
|
|
|
/** @var Profiler $profiler */ |
730
|
|
|
$profiler = new $class( |
731
|
|
|
[ 'sampling' => 1, 'output' => [ $output ] ] |
732
|
|
|
+ $wgProfiler |
733
|
|
|
+ [ 'threshold' => $wgProfileLimit ] |
734
|
|
|
); |
735
|
|
|
$profiler->setTemplated( true ); |
736
|
|
|
Profiler::replaceStubInstance( $profiler ); |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
$trxProfiler = Profiler::instance()->getTransactionProfiler(); |
740
|
|
|
$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) ); |
741
|
|
|
$trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ ); |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
/** |
745
|
|
|
* Clear all params and arguments. |
746
|
|
|
*/ |
747
|
|
|
public function clearParamsAndArgs() { |
748
|
|
|
$this->mOptions = []; |
749
|
|
|
$this->mArgs = []; |
750
|
|
|
$this->mInputLoaded = false; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* Load params and arguments from a given array |
755
|
|
|
* of command-line arguments |
756
|
|
|
* |
757
|
|
|
* @since 1.27 |
758
|
|
|
* @param array $argv |
759
|
|
|
*/ |
760
|
|
|
public function loadWithArgv( $argv ) { |
761
|
|
|
$options = []; |
762
|
|
|
$args = []; |
763
|
|
|
$this->orderedOptions = []; |
764
|
|
|
|
765
|
|
|
# Parse arguments |
766
|
|
|
for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) { |
767
|
|
|
if ( $arg == '--' ) { |
768
|
|
|
# End of options, remainder should be considered arguments |
769
|
|
|
$arg = next( $argv ); |
770
|
|
|
while ( $arg !== false ) { |
771
|
|
|
$args[] = $arg; |
772
|
|
|
$arg = next( $argv ); |
773
|
|
|
} |
774
|
|
|
break; |
775
|
|
|
} elseif ( substr( $arg, 0, 2 ) == '--' ) { |
776
|
|
|
# Long options |
777
|
|
|
$option = substr( $arg, 2 ); |
778
|
|
|
if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) { |
779
|
|
|
$param = next( $argv ); |
780
|
|
|
if ( $param === false ) { |
781
|
|
|
$this->error( "\nERROR: $option parameter needs a value after it\n" ); |
782
|
|
|
$this->maybeHelp( true ); |
783
|
|
|
} |
784
|
|
|
|
785
|
|
|
$this->setParam( $options, $option, $param ); |
786
|
|
|
} else { |
787
|
|
|
$bits = explode( '=', $option, 2 ); |
788
|
|
|
if ( count( $bits ) > 1 ) { |
789
|
|
|
$option = $bits[0]; |
790
|
|
|
$param = $bits[1]; |
791
|
|
|
} else { |
792
|
|
|
$param = 1; |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
$this->setParam( $options, $option, $param ); |
796
|
|
|
} |
797
|
|
|
} elseif ( $arg == '-' ) { |
798
|
|
|
# Lonely "-", often used to indicate stdin or stdout. |
799
|
|
|
$args[] = $arg; |
800
|
|
|
} elseif ( substr( $arg, 0, 1 ) == '-' ) { |
801
|
|
|
# Short options |
802
|
|
|
$argLength = strlen( $arg ); |
803
|
|
|
for ( $p = 1; $p < $argLength; $p++ ) { |
804
|
|
|
$option = $arg[$p]; |
805
|
|
|
if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) { |
806
|
|
|
$option = $this->mShortParamsMap[$option]; |
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) { |
810
|
|
|
$param = next( $argv ); |
811
|
|
|
if ( $param === false ) { |
812
|
|
|
$this->error( "\nERROR: $option parameter needs a value after it\n" ); |
813
|
|
|
$this->maybeHelp( true ); |
814
|
|
|
} |
815
|
|
|
$this->setParam( $options, $option, $param ); |
816
|
|
|
} else { |
817
|
|
|
$this->setParam( $options, $option, 1 ); |
818
|
|
|
} |
819
|
|
|
} |
820
|
|
|
} else { |
821
|
|
|
$args[] = $arg; |
822
|
|
|
} |
823
|
|
|
} |
824
|
|
|
|
825
|
|
|
$this->mOptions = $options; |
826
|
|
|
$this->mArgs = $args; |
827
|
|
|
$this->loadSpecialVars(); |
828
|
|
|
$this->mInputLoaded = true; |
829
|
|
|
} |
830
|
|
|
|
831
|
|
|
/** |
832
|
|
|
* Helper function used solely by loadParamsAndArgs |
833
|
|
|
* to prevent code duplication |
834
|
|
|
* |
835
|
|
|
* This sets the param in the options array based on |
836
|
|
|
* whether or not it can be specified multiple times. |
837
|
|
|
* |
838
|
|
|
* @since 1.27 |
839
|
|
|
* @param array $options |
840
|
|
|
* @param string $option |
841
|
|
|
* @param mixed $value |
842
|
|
|
*/ |
843
|
|
|
private function setParam( &$options, $option, $value ) { |
844
|
|
|
$this->orderedOptions[] = [ $option, $value ]; |
845
|
|
|
|
846
|
|
|
if ( isset( $this->mParams[$option] ) ) { |
847
|
|
|
$multi = $this->mParams[$option]['multiOccurrence']; |
848
|
|
|
} else { |
849
|
|
|
$multi = false; |
850
|
|
|
} |
851
|
|
|
$exists = array_key_exists( $option, $options ); |
852
|
|
|
if ( $multi && $exists ) { |
853
|
|
|
$options[$option][] = $value; |
854
|
|
|
} elseif ( $multi ) { |
855
|
|
|
$options[$option] = [ $value ]; |
856
|
|
|
} elseif ( !$exists ) { |
857
|
|
|
$options[$option] = $value; |
858
|
|
|
} else { |
859
|
|
|
$this->error( "\nERROR: $option parameter given twice\n" ); |
860
|
|
|
$this->maybeHelp( true ); |
861
|
|
|
} |
862
|
|
|
} |
863
|
|
|
|
864
|
|
|
/** |
865
|
|
|
* Process command line arguments |
866
|
|
|
* $mOptions becomes an array with keys set to the option names |
867
|
|
|
* $mArgs becomes a zero-based array containing the non-option arguments |
868
|
|
|
* |
869
|
|
|
* @param string $self The name of the script, if any |
870
|
|
|
* @param array $opts An array of options, in form of key=>value |
871
|
|
|
* @param array $args An array of command line arguments |
872
|
|
|
*/ |
873
|
|
|
public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) { |
874
|
|
|
# If we were given opts or args, set those and return early |
875
|
|
|
if ( $self ) { |
|
|
|
|
876
|
|
|
$this->mSelf = $self; |
877
|
|
|
$this->mInputLoaded = true; |
878
|
|
|
} |
879
|
|
|
if ( $opts ) { |
880
|
|
|
$this->mOptions = $opts; |
881
|
|
|
$this->mInputLoaded = true; |
882
|
|
|
} |
883
|
|
|
if ( $args ) { |
884
|
|
|
$this->mArgs = $args; |
885
|
|
|
$this->mInputLoaded = true; |
886
|
|
|
} |
887
|
|
|
|
888
|
|
|
# If we've already loaded input (either by user values or from $argv) |
889
|
|
|
# skip on loading it again. The array_shift() will corrupt values if |
890
|
|
|
# it's run again and again |
891
|
|
|
if ( $this->mInputLoaded ) { |
892
|
|
|
$this->loadSpecialVars(); |
893
|
|
|
|
894
|
|
|
return; |
895
|
|
|
} |
896
|
|
|
|
897
|
|
|
global $argv; |
898
|
|
|
$this->mSelf = $argv[0]; |
899
|
|
|
$this->loadWithArgv( array_slice( $argv, 1 ) ); |
900
|
|
|
} |
901
|
|
|
|
902
|
|
|
/** |
903
|
|
|
* Run some validation checks on the params, etc |
904
|
|
|
*/ |
905
|
|
|
protected function validateParamsAndArgs() { |
906
|
|
|
$die = false; |
907
|
|
|
# Check to make sure we've got all the required options |
908
|
|
|
foreach ( $this->mParams as $opt => $info ) { |
909
|
|
|
if ( $info['require'] && !$this->hasOption( $opt ) ) { |
910
|
|
|
$this->error( "Param $opt required!" ); |
911
|
|
|
$die = true; |
912
|
|
|
} |
913
|
|
|
} |
914
|
|
|
# Check arg list too |
915
|
|
|
foreach ( $this->mArgList as $k => $info ) { |
916
|
|
|
if ( $info['require'] && !$this->hasArg( $k ) ) { |
917
|
|
|
$this->error( 'Argument <' . $info['name'] . '> required!' ); |
918
|
|
|
$die = true; |
919
|
|
|
} |
920
|
|
|
} |
921
|
|
|
|
922
|
|
|
if ( $die ) { |
923
|
|
|
$this->maybeHelp( true ); |
924
|
|
|
} |
925
|
|
|
} |
926
|
|
|
|
927
|
|
|
/** |
928
|
|
|
* Handle the special variables that are global to all scripts |
929
|
|
|
*/ |
930
|
|
|
protected function loadSpecialVars() { |
931
|
|
|
if ( $this->hasOption( 'dbuser' ) ) { |
932
|
|
|
$this->mDbUser = $this->getOption( 'dbuser' ); |
933
|
|
|
} |
934
|
|
|
if ( $this->hasOption( 'dbpass' ) ) { |
935
|
|
|
$this->mDbPass = $this->getOption( 'dbpass' ); |
936
|
|
|
} |
937
|
|
|
if ( $this->hasOption( 'quiet' ) ) { |
938
|
|
|
$this->mQuiet = true; |
939
|
|
|
} |
940
|
|
|
if ( $this->hasOption( 'batch-size' ) ) { |
941
|
|
|
$this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); |
942
|
|
|
} |
943
|
|
|
} |
944
|
|
|
|
945
|
|
|
/** |
946
|
|
|
* Maybe show the help. |
947
|
|
|
* @param bool $force Whether to force the help to show, default false |
948
|
|
|
*/ |
949
|
|
|
protected function maybeHelp( $force = false ) { |
950
|
|
|
if ( !$force && !$this->hasOption( 'help' ) ) { |
951
|
|
|
return; |
952
|
|
|
} |
953
|
|
|
|
954
|
|
|
$screenWidth = 80; // TODO: Calculate this! |
955
|
|
|
$tab = " "; |
956
|
|
|
$descWidth = $screenWidth - ( 2 * strlen( $tab ) ); |
957
|
|
|
|
958
|
|
|
ksort( $this->mParams ); |
959
|
|
|
$this->mQuiet = false; |
960
|
|
|
|
961
|
|
|
// Description ... |
962
|
|
|
if ( $this->mDescription ) { |
963
|
|
|
$this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" ); |
964
|
|
|
} |
965
|
|
|
$output = "\nUsage: php " . basename( $this->mSelf ); |
966
|
|
|
|
967
|
|
|
// ... append parameters ... |
968
|
|
|
if ( $this->mParams ) { |
969
|
|
|
$output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]"; |
970
|
|
|
} |
971
|
|
|
|
972
|
|
|
// ... and append arguments. |
973
|
|
|
if ( $this->mArgList ) { |
974
|
|
|
$output .= ' '; |
975
|
|
|
foreach ( $this->mArgList as $k => $arg ) { |
976
|
|
|
if ( $arg['require'] ) { |
977
|
|
|
$output .= '<' . $arg['name'] . '>'; |
978
|
|
|
} else { |
979
|
|
|
$output .= '[' . $arg['name'] . ']'; |
980
|
|
|
} |
981
|
|
|
if ( $k < count( $this->mArgList ) - 1 ) { |
982
|
|
|
$output .= ' '; |
983
|
|
|
} |
984
|
|
|
} |
985
|
|
|
} |
986
|
|
|
$this->output( "$output\n\n" ); |
987
|
|
|
|
988
|
|
|
# TODO abstract some repetitive code below |
989
|
|
|
|
990
|
|
|
// Generic parameters |
991
|
|
|
$this->output( "Generic maintenance parameters:\n" ); |
992
|
|
|
foreach ( $this->mGenericParameters as $par => $info ) { |
993
|
|
|
if ( $info['shortName'] !== false ) { |
994
|
|
|
$par .= " (-{$info['shortName']})"; |
995
|
|
|
} |
996
|
|
|
$this->output( |
997
|
|
|
wordwrap( "$tab--$par: " . $info['desc'], $descWidth, |
998
|
|
|
"\n$tab$tab" ) . "\n" |
999
|
|
|
); |
1000
|
|
|
} |
1001
|
|
|
$this->output( "\n" ); |
1002
|
|
|
|
1003
|
|
|
$scriptDependantParams = $this->mDependantParameters; |
1004
|
|
View Code Duplication |
if ( count( $scriptDependantParams ) > 0 ) { |
1005
|
|
|
$this->output( "Script dependant parameters:\n" ); |
1006
|
|
|
// Parameters description |
1007
|
|
|
foreach ( $scriptDependantParams as $par => $info ) { |
1008
|
|
|
if ( $info['shortName'] !== false ) { |
1009
|
|
|
$par .= " (-{$info['shortName']})"; |
1010
|
|
|
} |
1011
|
|
|
$this->output( |
1012
|
|
|
wordwrap( "$tab--$par: " . $info['desc'], $descWidth, |
1013
|
|
|
"\n$tab$tab" ) . "\n" |
1014
|
|
|
); |
1015
|
|
|
} |
1016
|
|
|
$this->output( "\n" ); |
1017
|
|
|
} |
1018
|
|
|
|
1019
|
|
|
// Script specific parameters not defined on construction by |
1020
|
|
|
// Maintenance::addDefaultParams() |
1021
|
|
|
$scriptSpecificParams = array_diff_key( |
1022
|
|
|
# all script parameters: |
1023
|
|
|
$this->mParams, |
1024
|
|
|
# remove the Maintenance default parameters: |
1025
|
|
|
$this->mGenericParameters, |
1026
|
|
|
$this->mDependantParameters |
1027
|
|
|
); |
1028
|
|
View Code Duplication |
if ( count( $scriptSpecificParams ) > 0 ) { |
1029
|
|
|
$this->output( "Script specific parameters:\n" ); |
1030
|
|
|
// Parameters description |
1031
|
|
|
foreach ( $scriptSpecificParams as $par => $info ) { |
1032
|
|
|
if ( $info['shortName'] !== false ) { |
1033
|
|
|
$par .= " (-{$info['shortName']})"; |
1034
|
|
|
} |
1035
|
|
|
$this->output( |
1036
|
|
|
wordwrap( "$tab--$par: " . $info['desc'], $descWidth, |
1037
|
|
|
"\n$tab$tab" ) . "\n" |
1038
|
|
|
); |
1039
|
|
|
} |
1040
|
|
|
$this->output( "\n" ); |
1041
|
|
|
} |
1042
|
|
|
|
1043
|
|
|
// Print arguments |
1044
|
|
|
if ( count( $this->mArgList ) > 0 ) { |
1045
|
|
|
$this->output( "Arguments:\n" ); |
1046
|
|
|
// Arguments description |
1047
|
|
|
foreach ( $this->mArgList as $info ) { |
1048
|
|
|
$openChar = $info['require'] ? '<' : '['; |
1049
|
|
|
$closeChar = $info['require'] ? '>' : ']'; |
1050
|
|
|
$this->output( |
1051
|
|
|
wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " . |
1052
|
|
|
$info['desc'], $descWidth, "\n$tab$tab" ) . "\n" |
1053
|
|
|
); |
1054
|
|
|
} |
1055
|
|
|
$this->output( "\n" ); |
1056
|
|
|
} |
1057
|
|
|
|
1058
|
|
|
die( 1 ); |
|
|
|
|
1059
|
|
|
} |
1060
|
|
|
|
1061
|
|
|
/** |
1062
|
|
|
* Handle some last-minute setup here. |
1063
|
|
|
*/ |
1064
|
|
|
public function finalSetup() { |
1065
|
|
|
global $wgCommandLineMode, $wgShowSQLErrors, $wgServer; |
1066
|
|
|
global $wgDBadminuser, $wgDBadminpassword; |
1067
|
|
|
global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf; |
1068
|
|
|
|
1069
|
|
|
# Turn off output buffering again, it might have been turned on in the settings files |
1070
|
|
|
if ( ob_get_level() ) { |
1071
|
|
|
ob_end_flush(); |
1072
|
|
|
} |
1073
|
|
|
# Same with these |
1074
|
|
|
$wgCommandLineMode = true; |
1075
|
|
|
|
1076
|
|
|
# Override $wgServer |
1077
|
|
|
if ( $this->hasOption( 'server' ) ) { |
1078
|
|
|
$wgServer = $this->getOption( 'server', $wgServer ); |
1079
|
|
|
} |
1080
|
|
|
|
1081
|
|
|
# If these were passed, use them |
1082
|
|
|
if ( $this->mDbUser ) { |
1083
|
|
|
$wgDBadminuser = $this->mDbUser; |
1084
|
|
|
} |
1085
|
|
|
if ( $this->mDbPass ) { |
1086
|
|
|
$wgDBadminpassword = $this->mDbPass; |
1087
|
|
|
} |
1088
|
|
|
|
1089
|
|
|
if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) { |
1090
|
|
|
$wgDBuser = $wgDBadminuser; |
1091
|
|
|
$wgDBpassword = $wgDBadminpassword; |
1092
|
|
|
|
1093
|
|
|
if ( $wgDBservers ) { |
1094
|
|
|
/** |
1095
|
|
|
* @var $wgDBservers array |
1096
|
|
|
*/ |
1097
|
|
|
foreach ( $wgDBservers as $i => $server ) { |
1098
|
|
|
$wgDBservers[$i]['user'] = $wgDBuser; |
1099
|
|
|
$wgDBservers[$i]['password'] = $wgDBpassword; |
1100
|
|
|
} |
1101
|
|
|
} |
1102
|
|
|
if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) { |
1103
|
|
|
$wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser; |
1104
|
|
|
$wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword; |
1105
|
|
|
} |
1106
|
|
|
MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy(); |
1107
|
|
|
} |
1108
|
|
|
|
1109
|
|
|
// Per-script profiling; useful for debugging |
1110
|
|
|
$this->activateProfiler(); |
1111
|
|
|
|
1112
|
|
|
$this->afterFinalSetup(); |
1113
|
|
|
|
1114
|
|
|
$wgShowSQLErrors = true; |
1115
|
|
|
|
1116
|
|
|
MediaWiki\suppressWarnings(); |
1117
|
|
|
set_time_limit( 0 ); |
1118
|
|
|
MediaWiki\restoreWarnings(); |
1119
|
|
|
|
1120
|
|
|
$this->adjustMemoryLimit(); |
1121
|
|
|
} |
1122
|
|
|
|
1123
|
|
|
/** |
1124
|
|
|
* Execute a callback function at the end of initialisation |
1125
|
|
|
*/ |
1126
|
|
|
protected function afterFinalSetup() { |
1127
|
|
|
if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { |
1128
|
|
|
call_user_func( MW_CMDLINE_CALLBACK ); |
1129
|
|
|
} |
1130
|
|
|
} |
1131
|
|
|
|
1132
|
|
|
/** |
1133
|
|
|
* Potentially debug globals. Originally a feature only |
1134
|
|
|
* for refreshLinks |
1135
|
|
|
*/ |
1136
|
|
|
public function globals() { |
|
|
|
|
1137
|
|
|
if ( $this->hasOption( 'globals' ) ) { |
1138
|
|
|
print_r( $GLOBALS ); |
1139
|
|
|
} |
1140
|
|
|
} |
1141
|
|
|
|
1142
|
|
|
/** |
1143
|
|
|
* Generic setup for most installs. Returns the location of LocalSettings |
1144
|
|
|
* @return string |
1145
|
|
|
*/ |
1146
|
|
|
public function loadSettings() { |
1147
|
|
|
global $wgCommandLineMode, $IP; |
1148
|
|
|
|
1149
|
|
|
if ( isset( $this->mOptions['conf'] ) ) { |
1150
|
|
|
$settingsFile = $this->mOptions['conf']; |
1151
|
|
|
} elseif ( defined( "MW_CONFIG_FILE" ) ) { |
1152
|
|
|
$settingsFile = MW_CONFIG_FILE; |
1153
|
|
|
} else { |
1154
|
|
|
$settingsFile = "$IP/LocalSettings.php"; |
1155
|
|
|
} |
1156
|
|
|
if ( isset( $this->mOptions['wiki'] ) ) { |
1157
|
|
|
$bits = explode( '-', $this->mOptions['wiki'] ); |
1158
|
|
|
if ( count( $bits ) == 1 ) { |
1159
|
|
|
$bits[] = ''; |
1160
|
|
|
} |
1161
|
|
|
define( 'MW_DB', $bits[0] ); |
1162
|
|
|
define( 'MW_PREFIX', $bits[1] ); |
1163
|
|
|
} |
1164
|
|
|
|
1165
|
|
|
if ( !is_readable( $settingsFile ) ) { |
1166
|
|
|
$this->error( "A copy of your installation's LocalSettings.php\n" . |
1167
|
|
|
"must exist and be readable in the source directory.\n" . |
1168
|
|
|
"Use --conf to specify it.", true ); |
1169
|
|
|
} |
1170
|
|
|
$wgCommandLineMode = true; |
1171
|
|
|
|
1172
|
|
|
return $settingsFile; |
1173
|
|
|
} |
1174
|
|
|
|
1175
|
|
|
/** |
1176
|
|
|
* Support function for cleaning up redundant text records |
1177
|
|
|
* @param bool $delete Whether or not to actually delete the records |
1178
|
|
|
* @author Rob Church <[email protected]> |
1179
|
|
|
*/ |
1180
|
|
|
public function purgeRedundantText( $delete = true ) { |
1181
|
|
|
# Data should come off the master, wrapped in a transaction |
1182
|
|
|
$dbw = $this->getDB( DB_MASTER ); |
1183
|
|
|
$this->beginTransaction( $dbw, __METHOD__ ); |
|
|
|
|
1184
|
|
|
|
1185
|
|
|
# Get "active" text records from the revisions table |
1186
|
|
|
$cur = []; |
1187
|
|
|
$this->output( 'Searching for active text records in revisions table...' ); |
1188
|
|
|
$res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] ); |
1189
|
|
|
foreach ( $res as $row ) { |
1190
|
|
|
$cur[] = $row->rev_text_id; |
1191
|
|
|
} |
1192
|
|
|
$this->output( "done.\n" ); |
1193
|
|
|
|
1194
|
|
|
# Get "active" text records from the archive table |
1195
|
|
|
$this->output( 'Searching for active text records in archive table...' ); |
1196
|
|
|
$res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] ); |
1197
|
|
|
foreach ( $res as $row ) { |
1198
|
|
|
# old pre-MW 1.5 records can have null ar_text_id's. |
1199
|
|
|
if ( $row->ar_text_id !== null ) { |
1200
|
|
|
$cur[] = $row->ar_text_id; |
1201
|
|
|
} |
1202
|
|
|
} |
1203
|
|
|
$this->output( "done.\n" ); |
1204
|
|
|
|
1205
|
|
|
# Get the IDs of all text records not in these sets |
1206
|
|
|
$this->output( 'Searching for inactive text records...' ); |
1207
|
|
|
$cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )'; |
1208
|
|
|
$res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] ); |
1209
|
|
|
$old = []; |
1210
|
|
|
foreach ( $res as $row ) { |
1211
|
|
|
$old[] = $row->old_id; |
1212
|
|
|
} |
1213
|
|
|
$this->output( "done.\n" ); |
1214
|
|
|
|
1215
|
|
|
# Inform the user of what we're going to do |
1216
|
|
|
$count = count( $old ); |
1217
|
|
|
$this->output( "$count inactive items found.\n" ); |
1218
|
|
|
|
1219
|
|
|
# Delete as appropriate |
1220
|
|
|
if ( $delete && $count ) { |
1221
|
|
|
$this->output( 'Deleting...' ); |
1222
|
|
|
$dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ ); |
1223
|
|
|
$this->output( "done.\n" ); |
1224
|
|
|
} |
1225
|
|
|
|
1226
|
|
|
# Done |
1227
|
|
|
$this->commitTransaction( $dbw, __METHOD__ ); |
|
|
|
|
1228
|
|
|
} |
1229
|
|
|
|
1230
|
|
|
/** |
1231
|
|
|
* Get the maintenance directory. |
1232
|
|
|
* @return string |
1233
|
|
|
*/ |
1234
|
|
|
protected function getDir() { |
1235
|
|
|
return __DIR__; |
1236
|
|
|
} |
1237
|
|
|
|
1238
|
|
|
/** |
1239
|
|
|
* Returns a database to be used by current maintenance script. It can be set by setDB(). |
1240
|
|
|
* If not set, wfGetDB() will be used. |
1241
|
|
|
* This function has the same parameters as wfGetDB() |
1242
|
|
|
* |
1243
|
|
|
* @param integer $db DB index (DB_REPLICA/DB_MASTER) |
1244
|
|
|
* @param array $groups; default: empty array |
|
|
|
|
1245
|
|
|
* @param string|bool $wiki; default: current wiki |
|
|
|
|
1246
|
|
|
* @return Database |
1247
|
|
|
*/ |
1248
|
|
|
protected function getDB( $db, $groups = [], $wiki = false ) { |
1249
|
|
|
if ( is_null( $this->mDb ) ) { |
1250
|
|
|
return wfGetDB( $db, $groups, $wiki ); |
1251
|
|
|
} else { |
1252
|
|
|
return $this->mDb; |
1253
|
|
|
} |
1254
|
|
|
} |
1255
|
|
|
|
1256
|
|
|
/** |
1257
|
|
|
* Sets database object to be returned by getDB(). |
1258
|
|
|
* |
1259
|
|
|
* @param IDatabase $db Database object to be used |
1260
|
|
|
*/ |
1261
|
|
|
public function setDB( IDatabase $db ) { |
1262
|
|
|
$this->mDb = $db; |
|
|
|
|
1263
|
|
|
} |
1264
|
|
|
|
1265
|
|
|
/** |
1266
|
|
|
* Begin a transcation on a DB |
1267
|
|
|
* |
1268
|
|
|
* This method makes it clear that begin() is called from a maintenance script, |
1269
|
|
|
* which has outermost scope. This is safe, unlike $dbw->begin() called in other places. |
1270
|
|
|
* |
1271
|
|
|
* @param IDatabase $dbw |
1272
|
|
|
* @param string $fname Caller name |
1273
|
|
|
* @since 1.27 |
1274
|
|
|
*/ |
1275
|
|
|
protected function beginTransaction( IDatabase $dbw, $fname ) { |
1276
|
|
|
$dbw->begin( $fname ); |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
/** |
1280
|
|
|
* Commit the transcation on a DB handle and wait for replica DBs to catch up |
1281
|
|
|
* |
1282
|
|
|
* This method makes it clear that commit() is called from a maintenance script, |
1283
|
|
|
* which has outermost scope. This is safe, unlike $dbw->commit() called in other places. |
1284
|
|
|
* |
1285
|
|
|
* @param IDatabase $dbw |
1286
|
|
|
* @param string $fname Caller name |
1287
|
|
|
* @return bool Whether the replica DB wait succeeded |
1288
|
|
|
* @since 1.27 |
1289
|
|
|
*/ |
1290
|
|
|
protected function commitTransaction( IDatabase $dbw, $fname ) { |
1291
|
|
|
$dbw->commit( $fname ); |
1292
|
|
|
try { |
1293
|
|
|
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
1294
|
|
|
$lbFactory->waitForReplication( |
1295
|
|
|
[ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] |
1296
|
|
|
); |
1297
|
|
|
$this->lastReplicationWait = microtime( true ); |
1298
|
|
|
|
1299
|
|
|
return true; |
1300
|
|
|
} catch ( DBReplicationWaitError $e ) { |
1301
|
|
|
return false; |
1302
|
|
|
} |
1303
|
|
|
} |
1304
|
|
|
|
1305
|
|
|
/** |
1306
|
|
|
* Rollback the transcation on a DB handle |
1307
|
|
|
* |
1308
|
|
|
* This method makes it clear that rollback() is called from a maintenance script, |
1309
|
|
|
* which has outermost scope. This is safe, unlike $dbw->rollback() called in other places. |
1310
|
|
|
* |
1311
|
|
|
* @param IDatabase $dbw |
1312
|
|
|
* @param string $fname Caller name |
1313
|
|
|
* @since 1.27 |
1314
|
|
|
*/ |
1315
|
|
|
protected function rollbackTransaction( IDatabase $dbw, $fname ) { |
1316
|
|
|
$dbw->rollback( $fname ); |
1317
|
|
|
} |
1318
|
|
|
|
1319
|
|
|
/** |
1320
|
|
|
* Lock the search index |
1321
|
|
|
* @param Database &$db |
1322
|
|
|
*/ |
1323
|
|
|
private function lockSearchindex( $db ) { |
1324
|
|
|
$write = [ 'searchindex' ]; |
1325
|
|
|
$read = [ |
1326
|
|
|
'page', |
1327
|
|
|
'revision', |
1328
|
|
|
'text', |
1329
|
|
|
'interwiki', |
1330
|
|
|
'l10n_cache', |
1331
|
|
|
'user', |
1332
|
|
|
'page_restrictions' |
1333
|
|
|
]; |
1334
|
|
|
$db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ ); |
1335
|
|
|
} |
1336
|
|
|
|
1337
|
|
|
/** |
1338
|
|
|
* Unlock the tables |
1339
|
|
|
* @param Database &$db |
1340
|
|
|
*/ |
1341
|
|
|
private function unlockSearchindex( $db ) { |
1342
|
|
|
$db->unlockTables( __CLASS__ . '::' . __METHOD__ ); |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
/** |
1346
|
|
|
* Unlock and lock again |
1347
|
|
|
* Since the lock is low-priority, queued reads will be able to complete |
1348
|
|
|
* @param Database &$db |
1349
|
|
|
*/ |
1350
|
|
|
private function relockSearchindex( $db ) { |
1351
|
|
|
$this->unlockSearchindex( $db ); |
1352
|
|
|
$this->lockSearchindex( $db ); |
1353
|
|
|
} |
1354
|
|
|
|
1355
|
|
|
/** |
1356
|
|
|
* Perform a search index update with locking |
1357
|
|
|
* @param int $maxLockTime The maximum time to keep the search index locked. |
1358
|
|
|
* @param string $callback The function that will update the function. |
1359
|
|
|
* @param Database $dbw |
1360
|
|
|
* @param array $results |
1361
|
|
|
*/ |
1362
|
|
|
public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) { |
1363
|
|
|
$lockTime = time(); |
1364
|
|
|
|
1365
|
|
|
# Lock searchindex |
1366
|
|
|
if ( $maxLockTime ) { |
1367
|
|
|
$this->output( " --- Waiting for lock ---" ); |
1368
|
|
|
$this->lockSearchindex( $dbw ); |
1369
|
|
|
$lockTime = time(); |
1370
|
|
|
$this->output( "\n" ); |
1371
|
|
|
} |
1372
|
|
|
|
1373
|
|
|
# Loop through the results and do a search update |
1374
|
|
|
foreach ( $results as $row ) { |
1375
|
|
|
# Allow reads to be processed |
1376
|
|
|
if ( $maxLockTime && time() > $lockTime + $maxLockTime ) { |
1377
|
|
|
$this->output( " --- Relocking ---" ); |
1378
|
|
|
$this->relockSearchindex( $dbw ); |
1379
|
|
|
$lockTime = time(); |
1380
|
|
|
$this->output( "\n" ); |
1381
|
|
|
} |
1382
|
|
|
call_user_func( $callback, $dbw, $row ); |
1383
|
|
|
} |
1384
|
|
|
|
1385
|
|
|
# Unlock searchindex |
1386
|
|
|
if ( $maxLockTime ) { |
1387
|
|
|
$this->output( " --- Unlocking --" ); |
1388
|
|
|
$this->unlockSearchindex( $dbw ); |
1389
|
|
|
$this->output( "\n" ); |
1390
|
|
|
} |
1391
|
|
|
} |
1392
|
|
|
|
1393
|
|
|
/** |
1394
|
|
|
* Update the searchindex table for a given pageid |
1395
|
|
|
* @param Database $dbw A database write handle |
1396
|
|
|
* @param int $pageId The page ID to update. |
1397
|
|
|
* @return null|string |
1398
|
|
|
*/ |
1399
|
|
|
public function updateSearchIndexForPage( $dbw, $pageId ) { |
1400
|
|
|
// Get current revision |
1401
|
|
|
$rev = Revision::loadFromPageId( $dbw, $pageId ); |
1402
|
|
|
$title = null; |
1403
|
|
|
if ( $rev ) { |
1404
|
|
|
$titleObj = $rev->getTitle(); |
1405
|
|
|
$title = $titleObj->getPrefixedDBkey(); |
1406
|
|
|
$this->output( "$title..." ); |
1407
|
|
|
# Update searchindex |
1408
|
|
|
$u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() ); |
|
|
|
|
1409
|
|
|
$u->doUpdate(); |
1410
|
|
|
$this->output( "\n" ); |
1411
|
|
|
} |
1412
|
|
|
|
1413
|
|
|
return $title; |
1414
|
|
|
} |
1415
|
|
|
|
1416
|
|
|
/** |
1417
|
|
|
* Wrapper for posix_isatty() |
1418
|
|
|
* We default as considering stdin a tty (for nice readline methods) |
1419
|
|
|
* but treating stout as not a tty to avoid color codes |
1420
|
|
|
* |
1421
|
|
|
* @param mixed $fd File descriptor |
1422
|
|
|
* @return bool |
1423
|
|
|
*/ |
1424
|
|
|
public static function posix_isatty( $fd ) { |
1425
|
|
|
if ( !function_exists( 'posix_isatty' ) ) { |
1426
|
|
|
return !$fd; |
1427
|
|
|
} else { |
1428
|
|
|
return posix_isatty( $fd ); |
1429
|
|
|
} |
1430
|
|
|
} |
1431
|
|
|
|
1432
|
|
|
/** |
1433
|
|
|
* Prompt the console for input |
1434
|
|
|
* @param string $prompt What to begin the line with, like '> ' |
1435
|
|
|
* @return string Response |
1436
|
|
|
*/ |
1437
|
|
|
public static function readconsole( $prompt = '> ' ) { |
1438
|
|
|
static $isatty = null; |
1439
|
|
|
if ( is_null( $isatty ) ) { |
1440
|
|
|
$isatty = self::posix_isatty( 0 /*STDIN*/ ); |
1441
|
|
|
} |
1442
|
|
|
|
1443
|
|
|
if ( $isatty && function_exists( 'readline' ) ) { |
1444
|
|
|
$resp = readline( $prompt ); |
1445
|
|
|
if ( $resp === null ) { |
1446
|
|
|
// Workaround for https://github.com/facebook/hhvm/issues/4776 |
1447
|
|
|
return false; |
1448
|
|
|
} else { |
1449
|
|
|
return $resp; |
1450
|
|
|
} |
1451
|
|
|
} else { |
1452
|
|
|
if ( $isatty ) { |
1453
|
|
|
$st = self::readlineEmulation( $prompt ); |
1454
|
|
|
} else { |
1455
|
|
|
if ( feof( STDIN ) ) { |
1456
|
|
|
$st = false; |
1457
|
|
|
} else { |
1458
|
|
|
$st = fgets( STDIN, 1024 ); |
1459
|
|
|
} |
1460
|
|
|
} |
1461
|
|
|
if ( $st === false ) { |
1462
|
|
|
return false; |
1463
|
|
|
} |
1464
|
|
|
$resp = trim( $st ); |
1465
|
|
|
|
1466
|
|
|
return $resp; |
1467
|
|
|
} |
1468
|
|
|
} |
1469
|
|
|
|
1470
|
|
|
/** |
1471
|
|
|
* Emulate readline() |
1472
|
|
|
* @param string $prompt What to begin the line with, like '> ' |
1473
|
|
|
* @return string |
1474
|
|
|
*/ |
1475
|
|
|
private static function readlineEmulation( $prompt ) { |
1476
|
|
|
$bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] ); |
1477
|
|
|
if ( !wfIsWindows() && $bash ) { |
|
|
|
|
1478
|
|
|
$retval = false; |
1479
|
|
|
$encPrompt = wfEscapeShellArg( $prompt ); |
1480
|
|
|
$command = "read -er -p $encPrompt && echo \"\$REPLY\""; |
1481
|
|
|
$encCommand = wfEscapeShellArg( $command ); |
1482
|
|
|
$line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] ); |
1483
|
|
|
|
1484
|
|
|
if ( $retval == 0 ) { |
1485
|
|
|
return $line; |
1486
|
|
|
} elseif ( $retval == 127 ) { |
|
|
|
|
1487
|
|
|
// Couldn't execute bash even though we thought we saw it. |
1488
|
|
|
// Shell probably spit out an error message, sorry :( |
1489
|
|
|
// Fall through to fgets()... |
1490
|
|
|
} else { |
1491
|
|
|
// EOF/ctrl+D |
1492
|
|
|
return false; |
1493
|
|
|
} |
1494
|
|
|
} |
1495
|
|
|
|
1496
|
|
|
// Fallback... we'll have no editing controls, EWWW |
1497
|
|
|
if ( feof( STDIN ) ) { |
1498
|
|
|
return false; |
1499
|
|
|
} |
1500
|
|
|
print $prompt; |
1501
|
|
|
|
1502
|
|
|
return fgets( STDIN, 1024 ); |
1503
|
|
|
} |
1504
|
|
|
|
1505
|
|
|
/** |
1506
|
|
|
* Get the terminal size as a two-element array where the first element |
1507
|
|
|
* is the width (number of columns) and the second element is the height |
1508
|
|
|
* (number of rows). |
1509
|
|
|
* |
1510
|
|
|
* @return array |
1511
|
|
|
*/ |
1512
|
|
|
public static function getTermSize() { |
1513
|
|
|
$default = [ 80, 50 ]; |
1514
|
|
|
if ( wfIsWindows() ) { |
1515
|
|
|
return $default; |
1516
|
|
|
} |
1517
|
|
|
// It's possible to get the screen size with VT-100 terminal escapes, |
1518
|
|
|
// but reading the responses is not possible without setting raw mode |
1519
|
|
|
// (unless you want to require the user to press enter), and that |
1520
|
|
|
// requires an ioctl(), which we can't do. So we have to shell out to |
1521
|
|
|
// something that can do the relevant syscalls. There are a few |
1522
|
|
|
// options. Linux and Mac OS X both have "stty size" which does the |
1523
|
|
|
// job directly. |
1524
|
|
|
$retval = false; |
1525
|
|
|
$size = wfShellExec( 'stty size', $retval ); |
1526
|
|
|
if ( $retval !== 0 ) { |
1527
|
|
|
return $default; |
1528
|
|
|
} |
1529
|
|
|
if ( !preg_match( '/^(\d+) (\d+)$/', $size, $m ) ) { |
1530
|
|
|
return $default; |
1531
|
|
|
} |
1532
|
|
|
return [ intval( $m[2] ), intval( $m[1] ) ]; |
1533
|
|
|
} |
1534
|
|
|
|
1535
|
|
|
/** |
1536
|
|
|
* Call this to set up the autoloader to allow classes to be used from the |
1537
|
|
|
* tests directory. |
1538
|
|
|
*/ |
1539
|
|
|
public static function requireTestsAutoloader() { |
1540
|
|
|
require_once __DIR__ . '/../tests/common/TestsAutoLoader.php'; |
1541
|
|
|
} |
1542
|
|
|
} |
1543
|
|
|
|
1544
|
|
|
/** |
1545
|
|
|
* Fake maintenance wrapper, mostly used for the web installer/updater |
1546
|
|
|
*/ |
1547
|
|
|
class FakeMaintenance extends Maintenance { |
1548
|
|
|
protected $mSelf = "FakeMaintenanceScript"; |
1549
|
|
|
|
1550
|
|
|
public function execute() { |
1551
|
|
|
return; |
1552
|
|
|
} |
1553
|
|
|
} |
1554
|
|
|
|
1555
|
|
|
/** |
1556
|
|
|
* Class for scripts that perform database maintenance and want to log the |
1557
|
|
|
* update in `updatelog` so we can later skip it |
1558
|
|
|
*/ |
1559
|
|
|
abstract class LoggedUpdateMaintenance extends Maintenance { |
1560
|
|
|
public function __construct() { |
1561
|
|
|
parent::__construct(); |
1562
|
|
|
$this->addOption( 'force', 'Run the update even if it was completed already' ); |
1563
|
|
|
$this->setBatchSize( 200 ); |
1564
|
|
|
} |
1565
|
|
|
|
1566
|
|
|
public function execute() { |
1567
|
|
|
$db = $this->getDB( DB_MASTER ); |
1568
|
|
|
$key = $this->getUpdateKey(); |
1569
|
|
|
|
1570
|
|
|
if ( !$this->hasOption( 'force' ) |
1571
|
|
|
&& $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ ) |
1572
|
|
|
) { |
1573
|
|
|
$this->output( "..." . $this->updateSkippedMessage() . "\n" ); |
1574
|
|
|
|
1575
|
|
|
return true; |
1576
|
|
|
} |
1577
|
|
|
|
1578
|
|
|
if ( !$this->doDBUpdates() ) { |
1579
|
|
|
return false; |
1580
|
|
|
} |
1581
|
|
|
|
1582
|
|
View Code Duplication |
if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) { |
1583
|
|
|
return true; |
1584
|
|
|
} else { |
1585
|
|
|
$this->output( $this->updatelogFailedMessage() . "\n" ); |
1586
|
|
|
|
1587
|
|
|
return false; |
1588
|
|
|
} |
1589
|
|
|
} |
1590
|
|
|
|
1591
|
|
|
/** |
1592
|
|
|
* Message to show that the update was done already and was just skipped |
1593
|
|
|
* @return string |
1594
|
|
|
*/ |
1595
|
|
|
protected function updateSkippedMessage() { |
1596
|
|
|
$key = $this->getUpdateKey(); |
1597
|
|
|
|
1598
|
|
|
return "Update '{$key}' already logged as completed."; |
1599
|
|
|
} |
1600
|
|
|
|
1601
|
|
|
/** |
1602
|
|
|
* Message to show that the update log was unable to log the completion of this update |
1603
|
|
|
* @return string |
1604
|
|
|
*/ |
1605
|
|
|
protected function updatelogFailedMessage() { |
1606
|
|
|
$key = $this->getUpdateKey(); |
1607
|
|
|
|
1608
|
|
|
return "Unable to log update '{$key}' as completed."; |
1609
|
|
|
} |
1610
|
|
|
|
1611
|
|
|
/** |
1612
|
|
|
* Do the actual work. All child classes will need to implement this. |
1613
|
|
|
* Return true to log the update as done or false (usually on failure). |
1614
|
|
|
* @return bool |
1615
|
|
|
*/ |
1616
|
|
|
abstract protected function doDBUpdates(); |
1617
|
|
|
|
1618
|
|
|
/** |
1619
|
|
|
* Get the update key name to go in the update log table |
1620
|
|
|
* @return string |
1621
|
|
|
*/ |
1622
|
|
|
abstract protected function getUpdateKey(); |
1623
|
|
|
} |
1624
|
|
|
|
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.