Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.11% covered (danger)
19.11%
69 / 361
21.21% covered (danger)
21.21%
14 / 66
CRAP
0.00% covered (danger)
0.00%
0 / 1
Maintenance
19.17% covered (danger)
19.17%
69 / 360
21.21% covered (danger)
21.21%
14 / 66
14369.59
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getParameters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
n/a
0 / 0
n/a
0 / 0
0
 canExecuteWithoutLocalSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addOption
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 hasOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addArg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAllowUnregisteredOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasArg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArgs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setArg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBatchSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setBatchSize
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStdin
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 isQuiet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 output
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
 error
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 fatalError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 cleanupChanneled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 outputChanneled
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getDbType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addDefaultParams
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 getConfig
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getServiceContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireExtension
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkRequiredExtensions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 runChild
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 setup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 memoryLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clearParamsAndArgs
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 loadWithArgv
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
3.79
 loadParamsAndArgs
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 validateParamsAndArgs
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 loadSpecialVars
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
8.12
 maybeHelp
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 showHelp
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 finalSetup
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
240
 afterFinalSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purgeRedundantText
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
42
 getDir
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 setDB
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReplicaDB
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPrimaryDB
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setDBProvider
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 beginTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 commitTransaction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 waitForReplication
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 rollbackTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 countDown
n/a
0 / 0
n/a
0 / 0
5
 posix_isatty
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 readconsole
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 readlineEmulation
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 getTermSize
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 requireTestsAutoloader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHookContainer
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getHookRunner
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 parseIntList
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 validateUserOption
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
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 */
20
21use MediaWiki\Config\Config;
22use MediaWiki\HookContainer\HookContainer;
23use MediaWiki\HookContainer\HookRunner;
24use MediaWiki\MainConfigNames;
25use MediaWiki\Maintenance\MaintenanceParameters;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\Settings\SettingsBuilder;
28use MediaWiki\Shell\Shell;
29use MediaWiki\User\User;
30use Wikimedia\Rdbms\IConnectionProvider;
31use Wikimedia\Rdbms\IDatabase;
32use Wikimedia\Rdbms\IMaintainableDatabase;
33use Wikimedia\Rdbms\IReadableDatabase;
34
35// NOTE: MaintenanceParameters is needed in the constructor, and we may not have
36//       autoloading enabled at this point?
37require_once __DIR__ . '/MaintenanceParameters.php';
38
39/**
40 * Abstract maintenance class for quickly writing and churning out
41 * maintenance scripts with minimal effort. All that _must_ be defined
42 * is the execute() method. See docs/maintenance.txt for more info
43 * and a quick demo of how to use it.
44 *
45 * Terminology:
46 *   params: registry of named values that may be passed to the script
47 *   arg list: registry of positional values that may be passed to the script
48 *   options: passed param values
49 *   args: passed positional values
50 *
51 * In the command:
52 *   mwscript somescript.php --foo=bar baz
53 * foo is a param
54 * bar is the option value of the option for param foo
55 * baz is the arg value at index 0 in the arg list
56 *
57 * WARNING: the constructor, MaintenanceRunner::shouldExecute(), setup(), finalSetup(),
58 * and getName() are called before Setup.php is complete, which means most of the common
59 * infrastructure, like logging or autoloading, is not available. Be careful when changing
60 * these methods or the ones called from them. Likewise, be careful with the constructor
61 * when subclassing. MediaWikiServices instance is not yet available at this point.
62 *
63 * @stable to extend
64 *
65 * @since 1.16
66 * @ingroup Maintenance
67 */
68abstract class Maintenance {
69    /**
70     * Constants for DB access type
71     * @see Maintenance::getDbType()
72     */
73    public const DB_NONE = 0;
74    public const DB_STD = 1;
75    public const DB_ADMIN = 2;
76
77    // Const for getStdin()
78    public const STDIN_ALL = -1;
79
80    // Help group names
81    public const SCRIPT_DEPENDENT_PARAMETERS = 'Common options';
82    public const GENERIC_MAINTENANCE_PARAMETERS = 'Script runner options';
83
84    /**
85     * @var MaintenanceParameters
86     */
87    protected $parameters;
88
89    /**
90     * Empty.
91     * @deprecated since 1.39, use $this->parameters instead.
92     * @var array[]
93     * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
94     */
95    protected $mParams = [];
96
97    /**
98     * @var array This is the list of options that were actually passed
99     * @deprecated since 1.39, use {@see addOption} instead.
100     */
101    protected $mOptions = [];
102
103    /**
104     * @var array This is the list of arguments that were actually passed
105     * @deprecated since 1.39, use {@see addArg} instead.
106     */
107    protected $mArgs = [];
108
109    /** @var string|null Name of the script currently running */
110    protected $mSelf;
111
112    /** @var bool Special vars for params that are always used */
113    protected $mQuiet = false;
114    protected ?string $mDbUser = null;
115    protected ?string $mDbPass = null;
116
117    /**
118     * @var string A description of the script, children should change this via addDescription()
119     * @deprecated since 1.39, use {@see addDescription} instead.
120     */
121    protected $mDescription = '';
122
123    /**
124     * @var bool Have we already loaded our user input?
125     * @deprecated since 1.39, treat as private to the Maintenance base class
126     */
127    protected $mInputLoaded = false;
128
129    /**
130     * Batch size. If a script supports this, they should set
131     * a default with setBatchSize()
132     *
133     * @var int|null
134     */
135    protected $mBatchSize = null;
136
137    /**
138     * Used by getDB() / setDB()
139     * @var IMaintainableDatabase|null
140     */
141    private $mDb = null;
142
143    /** @var float UNIX timestamp */
144    private $lastReplicationWait = 0.0;
145
146    /**
147     * Used when creating separate schema files.
148     * @var resource|null
149     */
150    public $fileHandle;
151
152    /** @var HookContainer|null */
153    private $hookContainer;
154
155    /** @var HookRunner|null */
156    private $hookRunner;
157
158    /**
159     * Accessible via getConfig()
160     *
161     * @var Config|null
162     */
163    private $config;
164
165    /**
166     * @see Maintenance::requireExtension
167     * @var array
168     */
169    private $requiredExtensions = [];
170
171    /**
172     * Used to read the options in the order they were passed.
173     * Useful for option chaining (Ex. dumpBackup.php). It will
174     * be an empty array if the options are passed in through
175     * loadParamsAndArgs( $self, $opts, $args ).
176     *
177     * This is an array of arrays where
178     * 0 => the option and 1 => parameter value.
179     *
180     * @deprecated since 1.39, use $this->getParameters()->getOptionsSequence() instead.
181     * @var array
182     */
183    public $orderedOptions = [];
184    private ?IConnectionProvider $dbProvider = null;
185
186    /**
187     * Default constructor. Children should call this *first* if implementing
188     * their own constructors
189     *
190     * @stable to call
191     */
192    public function __construct() {
193        $this->parameters = new MaintenanceParameters();
194        $this->mOptions =& $this->parameters->getFieldReference( 'mOptions' );
195        $this->orderedOptions =& $this->parameters->getFieldReference( 'optionsSequence' );
196        $this->mArgs =& $this->parameters->getFieldReference( 'mArgs' );
197        $this->addDefaultParams();
198    }
199
200    /**
201     * @since 1.39
202     * @return MaintenanceParameters
203     */
204    public function getParameters() {
205        return $this->parameters;
206    }
207
208    /**
209     * Do the actual work. All child classes will need to implement this
210     *
211     * @return bool|null|void True for success, false for failure. Not returning
212     *   a value, or returning null, is also interpreted as success. Returning
213     *   false for failure will cause doMaintenance.php to exit the process
214     *   with a non-zero exit status.
215     */
216    abstract public function execute();
217
218    /**
219     * Whether this script can run without LocalSettings.php. Scripts that need to be able
220     * to run when MediaWiki has not been installed should override this to return true.
221     * Scripts that return true from this method must be able to function without
222     * a storage backend. When no LocalSettings.php file is present, any attempt to access
223     * the database will fail with a fatal error.
224     *
225     * @note Subclasses that override this method to return true should also override
226     * getDbType() to return self::DB_NONE, unless they are going to use the database
227     * connection when it is available.
228     *
229     * @see getDbType()
230     * @since 1.40
231     * @stable to override
232     * @return bool
233     */
234    public function canExecuteWithoutLocalSettings(): bool {
235        return false;
236    }
237
238    /**
239     * Checks to see if a particular option in supported.  Normally this means it
240     * has been registered by the script via addOption.
241     * @param string $name The name of the option
242     * @return bool true if the option exists, false otherwise
243     */
244    protected function supportsOption( $name ) {
245        return $this->parameters->supportsOption( $name );
246    }
247
248    /**
249     * Add a parameter to the script. Will be displayed on --help
250     * with the associated description
251     *
252     * @param string $name The name of the param (help, version, etc)
253     * @param string $description The description of the param to show on --help
254     * @param bool $required Is the param required?
255     * @param bool $withArg Is an argument required with this option?
256     * @param string|bool $shortName Character to use as short name
257     * @param bool $multiOccurrence Can this option be passed multiple times?
258     */
259    protected function addOption( $name, $description, $required = false,
260        $withArg = false, $shortName = false, $multiOccurrence = false
261    ) {
262        $this->parameters->addOption(
263            $name,
264            $description,
265            $required,
266            $withArg,
267            $shortName,
268            $multiOccurrence
269        );
270    }
271
272    /**
273     * Checks to see if a particular option was set.
274     *
275     * @param string $name The name of the option
276     * @return bool
277     */
278    protected function hasOption( $name ) {
279        return $this->parameters->hasOption( $name );
280    }
281
282    /**
283     * Get an option, or return the default.
284     *
285     * If the option was added to support multiple occurrences,
286     * this will return an array.
287     *
288     * @param string $name The name of the param
289     * @param mixed|null $default Anything you want, default null
290     * @return mixed
291     * @return-taint none
292     */
293    protected function getOption( $name, $default = null ) {
294        return $this->parameters->getOption( $name, $default );
295    }
296
297    /**
298     * Add some args that are needed
299     * @param string $arg Name of the arg, like 'start'
300     * @param string $description Short description of the arg
301     * @param bool $required Is this required?
302     * @param bool $multi Does it allow multiple values? (Last arg only)
303     */
304    protected function addArg( $arg, $description, $required = true, $multi = false ) {
305        $this->parameters->addArg( $arg, $description, $required, $multi );
306    }
307
308    /**
309     * Remove an option.  Useful for removing options that won't be used in your script.
310     * @param string $name The option to remove.
311     */
312    protected function deleteOption( $name ) {
313        $this->parameters->deleteOption( $name );
314    }
315
316    /**
317     * Sets whether to allow unregistered options, which are options passed to
318     * a script that do not match an expected parameter.
319     * @param bool $allow Should we allow?
320     */
321    protected function setAllowUnregisteredOptions( $allow ) {
322        $this->parameters->setAllowUnregisteredOptions( $allow );
323    }
324
325    /**
326     * Set the description text.
327     * @param string $text The text of the description
328     */
329    protected function addDescription( $text ) {
330        $this->parameters->setDescription( $text );
331    }
332
333    /**
334     * Does a given argument exist?
335     * @param int|string $argId The index (from zero) of the argument, or
336     *                   the name declared for the argument by addArg().
337     * @return bool
338     */
339    protected function hasArg( $argId = 0 ) {
340        return $this->parameters->hasArg( $argId );
341    }
342
343    /**
344     * Get an argument.
345     * @param int|string $argId The index (from zero) of the argument, or
346     *                   the name declared for the argument by addArg().
347     * @param mixed|null $default The default if it doesn't exist
348     * @return mixed
349     * @return-taint none
350     */
351    protected function getArg( $argId = 0, $default = null ) {
352        return $this->parameters->getArg( $argId, $default );
353    }
354
355    /**
356     * Get arguments.
357     * @since 1.40
358     *
359     * @param int|string $offset The index (from zero) of the first argument, or
360     *                   the name declared for the argument by addArg().
361     * @return string[]
362     */
363    protected function getArgs( $offset = 0 ) {
364        return $this->parameters->getArgs( $offset );
365    }
366
367    /**
368     * Programmatically set the value of the given option.
369     * Useful for setting up child scripts, see runChild().
370     *
371     * @since 1.39
372     *
373     * @param string $name
374     * @param mixed|null $value
375     */
376    public function setOption( string $name, $value ): void {
377        $this->parameters->setOption( $name, $value );
378    }
379
380    /**
381     * Programmatically set the value of the given argument.
382     * Useful for setting up child scripts, see runChild().
383     *
384     * @since 1.39
385     *
386     * @param string|int $argId Arg index or name
387     * @param mixed|null $value
388     */
389    public function setArg( $argId, $value ): void {
390        $this->parameters->setArg( $argId, $value );
391    }
392
393    /**
394     * Returns batch size
395     *
396     * @since 1.31
397     *
398     * @return int|null
399     */
400    protected function getBatchSize() {
401        return $this->mBatchSize;
402    }
403
404    /**
405     * @param int $s The number of operations to do in a batch
406     */
407    protected function setBatchSize( $s = 0 ) {
408        $this->mBatchSize = $s;
409
410        // If we support $mBatchSize, show the option.
411        // Used to be in addDefaultParams, but in order for that to
412        // work, subclasses would have to call this function in the constructor
413        // before they called parent::__construct which is just weird
414        // (and really wasn't done).
415        if ( $this->mBatchSize ) {
416            $this->addOption( 'batch-size', 'Run this many operations ' .
417                'per batch, default: ' . $this->mBatchSize, false, true );
418            if ( $this->supportsOption( 'batch-size' ) ) {
419                // This seems a little ugly...
420                $this->parameters->assignGroup( self::SCRIPT_DEPENDENT_PARAMETERS, [ 'batch-size' ] );
421            }
422        }
423    }
424
425    /**
426     * Get the script's name
427     * @return string
428     */
429    public function getName() {
430        return $this->mSelf;
431    }
432
433    /**
434     * Return input from stdin.
435     * @param int|null $len The number of bytes to read. If null, just
436     * return the handle. Maintenance::STDIN_ALL returns the full content
437     * @return mixed
438     */
439    protected function getStdin( $len = null ) {
440        if ( $len == self::STDIN_ALL ) {
441            return file_get_contents( 'php://stdin' );
442        }
443        $f = fopen( 'php://stdin', 'rt' );
444        if ( !$len ) {
445            return $f;
446        }
447        $input = fgets( $f, $len );
448        fclose( $f );
449
450        return rtrim( $input );
451    }
452
453    /**
454     * @return bool
455     */
456    public function isQuiet() {
457        return $this->mQuiet;
458    }
459
460    /**
461     * Throw some output to the user. Scripts can call this with no fears,
462     * as we handle all --quiet stuff here
463     * @stable to override
464     * @param string $out The text to show to the user
465     * @param mixed|null $channel Unique identifier for the channel. See function outputChanneled.
466     */
467    protected function output( $out, $channel = null ) {
468        // This is sometimes called very early, before Setup.php is included.
469        if ( defined( 'MW_SERVICE_BOOTSTRAP_COMPLETE' ) ) {
470            // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385)
471            $stats = $this->getServiceContainer()->getStatsdDataFactory();
472            if ( $stats->getDataCount() > 1000 ) {
473                MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() );
474            }
475        }
476
477        if ( $this->mQuiet ) {
478            return;
479        }
480        if ( $channel === null ) {
481            $this->cleanupChanneled();
482            print $out;
483        } else {
484            $out = preg_replace( '/\n\z/', '', $out );
485            $this->outputChanneled( $out, $channel );
486        }
487    }
488
489    /**
490     * Throw an error to the user. Doesn't respect --quiet, so don't use
491     * this for non-error output
492     * @stable to override
493     * @param string|StatusValue $err The error to display
494     * @param int $die Deprecated since 1.31, use Maintenance::fatalError() instead
495     */
496    protected function error( $err, $die = 0 ) {
497        if ( intval( $die ) !== 0 ) {
498            wfDeprecated( __METHOD__ . '( $err, $die )', '1.31' );
499            $this->fatalError( $err, intval( $die ) );
500        }
501        if ( $err instanceof StatusValue ) {
502            foreach ( [ 'warning' => 'Warning: ', 'error' => 'Error: ' ] as $type => $prefix ) {
503                foreach ( $err->getMessages( $type ) as $msg ) {
504                    $this->error(
505                        $prefix . wfMessage( $msg )
506                            ->inLanguage( 'en' )
507                            ->useDatabase( false )
508                            ->text()
509                    );
510                }
511            }
512            return;
513        }
514        $this->outputChanneled( false );
515        if (
516            ( PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg' ) &&
517            !defined( 'MW_PHPUNIT_TEST' )
518        ) {
519            fwrite( STDERR, $err . "\n" );
520        } else {
521            print $err . "\n";
522        }
523    }
524
525    /**
526     * Output a message and terminate the current script.
527     *
528     * @stable to override
529     * @param string|StatusValue $msg Error message
530     * @param int $exitCode PHP exit status. Should be in range 1-254.
531     * @since 1.31
532     * @return never
533     */
534    protected function fatalError( $msg, $exitCode = 1 ) {
535        $this->error( $msg );
536        exit( $exitCode );
537    }
538
539    private $atLineStart = true;
540    private $lastChannel = null;
541
542    /**
543     * Clean up channeled output.  Output a newline if necessary.
544     */
545    public function cleanupChanneled() {
546        if ( !$this->atLineStart ) {
547            print "\n";
548            $this->atLineStart = true;
549        }
550    }
551
552    /**
553     * Message outputter with channeled message support. Messages on the
554     * same channel are concatenated, but any intervening messages in another
555     * channel start a new line.
556     * @param string|false $msg The message without trailing newline
557     * @param string|null $channel Channel identifier or null for no
558     *     channel. Channel comparison uses ===.
559     */
560    public function outputChanneled( $msg, $channel = null ) {
561        if ( $msg === false ) {
562            $this->cleanupChanneled();
563
564            return;
565        }
566
567        // End the current line if necessary
568        if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
569            print "\n";
570        }
571
572        print $msg;
573
574        $this->atLineStart = false;
575        if ( $channel === null ) {
576            // For unchanneled messages, output trailing newline immediately
577            print "\n";
578            $this->atLineStart = true;
579        }
580        $this->lastChannel = $channel;
581    }
582
583    /**
584     * Does the script need different DB access? By default, we give Maintenance
585     * scripts normal rights to the DB. Sometimes, a script needs admin rights
586     * access for a reason and sometimes they want no access. Subclasses should
587     * override and return one of the following values, as needed:
588     *    Maintenance::DB_NONE  -  For no DB access at all
589     *    Maintenance::DB_STD   -  For normal DB access, default
590     *    Maintenance::DB_ADMIN -  For admin DB access
591     *
592     * @note Subclasses that override this method to return self::DB_NONE should
593     * also override canExecuteWithoutLocalSettings() to return true, unless they
594     * need the wiki to be set up for reasons beyond access to a database connection.
595     *
596     * @see canExecuteWithoutLocalSettings()
597     * @stable to override
598     * @return int
599     */
600    public function getDbType() {
601        return self::DB_STD;
602    }
603
604    /**
605     * Add the default parameters to the scripts
606     */
607    protected function addDefaultParams() {
608        # Generic (non-script-dependent) options:
609
610        $this->addOption( 'help', 'Display this help message', false, false, 'h' );
611        $this->addOption( 'quiet', 'Whether to suppress non-error output', false, false, 'q' );
612
613        # Save generic options to display them separately in help
614        $generic = [ 'help', 'quiet' ];
615        $this->parameters->assignGroup( self::GENERIC_MAINTENANCE_PARAMETERS, $generic );
616
617        # Script-dependent options:
618
619        // If we support a DB, show the options
620        if ( $this->getDbType() > 0 ) {
621            $this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
622            $this->addOption( 'dbpass', 'The password to use for this script', false, true );
623            $this->addOption( 'dbgroupdefault', 'The default DB group to use.', false, true );
624        }
625
626        # Save additional script-dependent options to display
627        # them separately in help
628        $dependent = array_diff(
629            $this->parameters->getOptionNames(),
630            $generic
631        );
632        $this->parameters->assignGroup( self::SCRIPT_DEPENDENT_PARAMETERS, $dependent );
633    }
634
635    /**
636     * @since 1.24
637     * @stable to override
638     * @return Config
639     */
640    public function getConfig() {
641        if ( $this->config === null ) {
642            $this->config = $this->getServiceContainer()->getMainConfig();
643        }
644
645        return $this->config;
646    }
647
648    /**
649     * Returns the main service container.
650     *
651     * @since 1.40
652     * @return MediaWikiServices
653     */
654    protected function getServiceContainer() {
655        return MediaWikiServices::getInstance();
656    }
657
658    /**
659     * @since 1.24
660     * @param Config $config
661     */
662    public function setConfig( Config $config ) {
663        $this->config = $config;
664    }
665
666    /**
667     * Indicate that the specified extension must be
668     * loaded before the script can run.
669     *
670     * This *must* be called in the constructor.
671     *
672     * @since 1.28
673     * @param string $name
674     */
675    protected function requireExtension( $name ) {
676        $this->requiredExtensions[] = $name;
677    }
678
679    /**
680     * Verify that the required extensions are installed
681     *
682     * @since 1.28
683     */
684    public function checkRequiredExtensions() {
685        $registry = ExtensionRegistry::getInstance();
686        $missing = [];
687        foreach ( $this->requiredExtensions as $name ) {
688            if ( !$registry->isLoaded( $name ) ) {
689                $missing[] = $name;
690            }
691        }
692
693        if ( $missing ) {
694            if ( count( $missing ) === 1 ) {
695                $msg = 'The "' . $missing[ 0 ] . '" extension must be installed for this script to run. '
696                    . 'Please enable it and then try again.';
697            } else {
698                $msg = 'The following extensions must be installed for this script to run: "'
699                    . implode( '", "', $missing ) . '". Please enable them and then try again.';
700            }
701            $this->fatalError( $msg );
702        }
703    }
704
705    /**
706     * Run a child maintenance script. Pass all of the current arguments
707     * to it.
708     * @param string $maintClass A name of a child maintenance class
709     * @param string|null $classFile Full path of where the child is
710     * @stable to override
711     * @return Maintenance
712     */
713    public function runChild( $maintClass, $classFile = null ) {
714        // Make sure the class is loaded first
715        if ( !class_exists( $maintClass ) ) {
716            if ( $classFile ) {
717                require_once $classFile;
718            }
719            if ( !class_exists( $maintClass ) ) {
720                $this->fatalError( "Cannot spawn child: $maintClass" );
721            }
722        }
723
724        /**
725         * @var Maintenance $child
726         */
727        $child = new $maintClass();
728        $child->loadParamsAndArgs(
729            $this->mSelf,
730            $this->parameters->getOptions(),
731            $this->parameters->getArgs()
732        );
733        if ( $this->mDb !== null ) {
734            $child->setDB( $this->mDb );
735        }
736        if ( $this->dbProvider !== null ) {
737            $child->setDBProvider( $this->dbProvider );
738        }
739
740        return $child;
741    }
742
743    /**
744     * Provides subclasses with an opportunity to perform initial checks.
745     * @stable to override
746     */
747    public function setup() {
748        // noop
749    }
750
751    /**
752     * Normally we disable the memory_limit when running admin scripts.
753     * Some scripts may wish to actually set a limit, however, to avoid
754     * blowing up unexpectedly.
755     * @stable to override
756     * @return string
757     */
758    public function memoryLimit() {
759        return 'max';
760    }
761
762    /**
763     * Clear all params and arguments.
764     */
765    public function clearParamsAndArgs() {
766        $this->parameters->clear();
767        $this->mInputLoaded = false;
768    }
769
770    /**
771     * @since 1.40
772     * @internal
773     * @param string $name
774     */
775    public function setName( string $name ) {
776        $this->mSelf = $name;
777        $this->parameters->setName( $this->mSelf );
778    }
779
780    /**
781     * Load params and arguments from a given array
782     * of command-line arguments
783     *
784     * @since 1.27
785     * @param array $argv The argument array, not including the script itself.
786     */
787    public function loadWithArgv( $argv ) {
788        if ( $this->mDescription ) {
789            $this->parameters->setDescription( $this->mDescription );
790        }
791
792        $this->parameters->loadWithArgv( $argv );
793
794        if ( $this->parameters->hasErrors() ) {
795            $errors = "\nERROR: " . implode( "\nERROR: ", $this->parameters->getErrors() ) . "\n";
796            $this->error( $errors );
797            $this->maybeHelp( true );
798        }
799
800        $this->loadSpecialVars();
801        $this->mInputLoaded = true;
802    }
803
804    /**
805     * Process command line arguments when running as a child script
806     *
807     * @param string|null $self The name of the script, if any
808     * @param array|null $opts An array of options, in form of key=>value
809     * @param array|null $args An array of command line arguments
810     */
811    public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
812        # If we were given opts or args, set those and return early
813        if ( $self !== null || $opts !== null || $args !== null ) {
814            if ( $self !== null ) {
815                $this->mSelf = $self;
816                $this->parameters->setName( $self );
817            }
818            $this->parameters->setOptionsAndArgs( $opts ?? [], $args ?? [] );
819            $this->mInputLoaded = true;
820        }
821
822        # If we've already loaded input (either by user values or from $argv)
823        # skip on loading it again.
824        if ( $this->mInputLoaded ) {
825            $this->loadSpecialVars();
826
827            return;
828        }
829
830        global $argv;
831        $this->mSelf = $argv[0];
832        $this->parameters->setName( $this->mSelf );
833        $this->loadWithArgv( array_slice( $argv, 1 ) );
834    }
835
836    /**
837     * Run some validation checks on the params, etc
838     * @stable to override
839     */
840    public function validateParamsAndArgs() {
841        $valid = $this->parameters->validate();
842
843        $this->maybeHelp( !$valid );
844    }
845
846    /**
847     * Handle the special variables that are global to all scripts
848     * @stable to override
849     */
850    protected function loadSpecialVars() {
851        if ( $this->hasOption( 'dbuser' ) ) {
852            $this->mDbUser = $this->getOption( 'dbuser' );
853        }
854        if ( $this->hasOption( 'dbpass' ) ) {
855            $this->mDbPass = $this->getOption( 'dbpass' );
856        }
857        if ( $this->hasOption( 'quiet' ) ) {
858            $this->mQuiet = true;
859        }
860        if ( $this->hasOption( 'batch-size' ) ) {
861            $this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
862        }
863    }
864
865    /**
866     * Maybe show the help. If the help is shown, exit.
867     *
868     * @param bool $force Whether to force the help to show, default false
869     */
870    protected function maybeHelp( $force = false ) {
871        if ( !$force && !$this->hasOption( 'help' ) ) {
872            return;
873        }
874
875        if ( $this->parameters->hasErrors() && !$this->hasOption( 'help' ) ) {
876            $errors = "\nERROR: " . implode( "\nERROR: ", $this->parameters->getErrors() ) . "\n";
877            $this->error( $errors );
878        }
879
880        $this->showHelp();
881        die( 1 );
882    }
883
884    /**
885     * Definitely show the help. Does not exit.
886     */
887    protected function showHelp() {
888        $this->mQuiet = false;
889        $help = $this->parameters->getHelp();
890        $this->output( $help );
891    }
892
893    /**
894     * Handle some last-minute setup here.
895     *
896     * @stable to override
897     *
898     * @param SettingsBuilder $settingsBuilder
899     */
900    public function finalSetup( SettingsBuilder $settingsBuilder ) {
901        $config = $settingsBuilder->getConfig();
902        $overrides = [];
903        $overrides['DBadminuser'] = $config->get( MainConfigNames::DBadminuser );
904        $overrides['DBadminpassword'] = $config->get( MainConfigNames::DBadminpassword );
905
906        # Turn off output buffering again, it might have been turned on in the settings files
907        if ( ob_get_level() ) {
908            ob_end_flush();
909        }
910
911        # Override $wgServer
912        if ( $this->hasOption( 'server' ) ) {
913            $overrides['Server'] = $this->getOption( 'server', $config->get( MainConfigNames::Server ) );
914        }
915
916        # If these were passed, use them
917        if ( $this->mDbUser ) {
918            $overrides['DBadminuser'] = $this->mDbUser;
919        }
920        if ( $this->mDbPass ) {
921            $overrides['DBadminpassword'] = $this->mDbPass;
922        }
923        if ( $this->hasOption( 'dbgroupdefault' ) ) {
924            $overrides['DBDefaultGroup'] = $this->getOption( 'dbgroupdefault', null );
925            // TODO: once MediaWikiServices::getInstance() starts throwing exceptions
926            // and not deprecation warnings for premature access to service container,
927            // we can remove this line. This method is called before Setup.php,
928            // so it would be guaranteed DBLoadBalancerFactory is not yet initialized.
929            if ( MediaWikiServices::hasInstance() ) {
930                $service = $this->getServiceContainer()->peekService( 'DBLoadBalancerFactory' );
931                if ( $service ) {
932                    $service->destroy();
933                }
934            }
935        }
936
937        if ( $this->getDbType() == self::DB_ADMIN && isset( $overrides[ 'DBadminuser' ] ) ) {
938            $overrides['DBuser'] = $overrides[ 'DBadminuser' ];
939            $overrides['DBpassword'] = $overrides[ 'DBadminpassword' ];
940
941            /** @var array $dbServers */
942            $dbServers = $config->get( MainConfigNames::DBservers );
943            if ( $dbServers ) {
944                foreach ( $dbServers as $i => $server ) {
945                    $dbServers[$i]['user'] = $overrides['DBuser'];
946                    $dbServers[$i]['password'] = $overrides['DBpassword'];
947                }
948                $overrides['DBservers'] = $dbServers;
949            }
950
951            $lbFactoryConf = $config->get( MainConfigNames::LBFactoryConf );
952            if ( isset( $lbFactoryConf['serverTemplate'] ) ) {
953                $lbFactoryConf['serverTemplate']['user'] = $overrides['DBuser'];
954                $lbFactoryConf['serverTemplate']['password'] = $overrides['DBpassword'];
955                $overrides['LBFactoryConf'] = $lbFactoryConf;
956            }
957
958            // TODO: once MediaWikiServices::getInstance() starts throwing exceptions
959            // and not deprecation warnings for premature access to service container,
960            // we can remove this line. This method is called before Setup.php,
961            // so it would be guaranteed DBLoadBalancerFactory is not yet initialized.
962            if ( MediaWikiServices::hasInstance() ) {
963                $service = $this->getServiceContainer()->peekService( 'DBLoadBalancerFactory' );
964                if ( $service ) {
965                    $service->destroy();
966                }
967            }
968        }
969
970        $this->afterFinalSetup();
971
972        $overrides['ShowExceptionDetails'] = true;
973        $overrides['ShowHostname'] = true;
974
975        ini_set( 'max_execution_time', '0' );
976        $settingsBuilder->putConfigValues( $overrides );
977    }
978
979    /**
980     * Override to perform any required operation at the end of initialisation
981     * @stable to override
982     */
983    protected function afterFinalSetup() {
984    }
985
986    /**
987     * Support function for cleaning up redundant text records
988     * @param bool $delete Whether or not to actually delete the records
989     * @author Rob Church <robchur@gmail.com>
990     */
991    public function purgeRedundantText( $delete = true ) {
992        # Data should come off the master, wrapped in a transaction
993        $dbw = $this->getPrimaryDB();
994        $this->beginTransaction( $dbw, __METHOD__ );
995
996        # Get "active" text records via the content table
997        $cur = [];
998        $this->output( 'Searching for active text records via contents table...' );
999        $res = $dbw->newSelectQueryBuilder()
1000            ->select( 'content_address' )
1001            ->distinct()
1002            ->from( 'content' )
1003            ->caller( __METHOD__ )->fetchResultSet();
1004        $blobStore = $this->getServiceContainer()->getBlobStore();
1005        foreach ( $res as $row ) {
1006            // @phan-suppress-next-line PhanUndeclaredMethod
1007            $textId = $blobStore->getTextIdFromAddress( $row->content_address );
1008            if ( $textId ) {
1009                $cur[] = $textId;
1010            }
1011        }
1012        $this->output( "done.\n" );
1013
1014        # Get the IDs of all text records not in these sets
1015        $this->output( 'Searching for inactive text records...' );
1016        $res = $dbw->newSelectQueryBuilder()
1017            ->select( 'old_id' )
1018            ->distinct()
1019            ->from( 'text' )
1020            ->where( $dbw->expr( 'old_id', '!=', $cur ) )
1021            ->caller( __METHOD__ )->fetchResultSet();
1022        $old = [];
1023        foreach ( $res as $row ) {
1024            $old[] = $row->old_id;
1025        }
1026        $this->output( "done.\n" );
1027
1028        # Inform the user of what we're going to do
1029        $count = count( $old );
1030        $this->output( "$count inactive items found.\n" );
1031
1032        # Delete as appropriate
1033        if ( $delete && $count ) {
1034            $this->output( 'Deleting...' );
1035            $dbw->newDeleteQueryBuilder()
1036                ->deleteFrom( 'text' )
1037                ->where( [ 'old_id' => $old ] )
1038                ->caller( __METHOD__ )
1039                ->execute();
1040            $this->output( "done.\n" );
1041        }
1042
1043        $this->commitTransaction( $dbw, __METHOD__ );
1044    }
1045
1046    /**
1047     * Get the maintenance directory.
1048     * @return string
1049     */
1050    protected function getDir() {
1051        return __DIR__ . '/../';
1052    }
1053
1054    /**
1055     * Returns a database to be used by current maintenance script.
1056     *
1057     * This uses the main LBFactory instance by default unless overriden via setDB().
1058     *
1059     * This function has the same parameters as LoadBalancer::getConnection().
1060     *
1061     * For simple cases, use ::getReplicaDB() or ::getPrimaryDB() instead.
1062     *
1063     * @stable to override
1064     *
1065     * @param int $db DB index (DB_REPLICA/DB_PRIMARY)
1066     * @param string|string[] $groups default: empty array
1067     * @param string|bool $dbDomain default: current wiki
1068     * @return IMaintainableDatabase
1069     */
1070    protected function getDB( $db, $groups = [], $dbDomain = false ) {
1071        if ( $this->mDb === null ) {
1072            return $this->getServiceContainer()
1073                ->getDBLoadBalancerFactory()
1074                ->getMainLB( $dbDomain )
1075                ->getMaintenanceConnectionRef( $db, $groups, $dbDomain );
1076        }
1077
1078        return $this->mDb;
1079    }
1080
1081    /**
1082     * Sets database object to be returned by getDB().
1083     * @stable to override
1084     *
1085     * @param IMaintainableDatabase $db
1086     */
1087    public function setDB( IMaintainableDatabase $db ) {
1088        $this->mDb = $db;
1089    }
1090
1091    /**
1092     * @return IReadableDatabase
1093     * @since 1.42
1094     */
1095    protected function getReplicaDB(): IReadableDatabase {
1096        if ( $this->dbProvider === null ) {
1097            $this->dbProvider = $this->getServiceContainer()->getConnectionProvider();
1098        }
1099        return $this->dbProvider->getReplicaDatabase();
1100    }
1101
1102    /**
1103     * @return IDatabase
1104     * @since 1.42
1105     */
1106    protected function getPrimaryDB(): IDatabase {
1107        if ( $this->dbProvider === null ) {
1108            $this->dbProvider = $this->getServiceContainer()->getConnectionProvider();
1109        }
1110        return $this->dbProvider->getPrimaryDatabase();
1111    }
1112
1113    /**
1114     * @internal
1115     * @param IConnectionProvider $dbProvider
1116     * @return void
1117     */
1118    public function setDBProvider( IConnectionProvider $dbProvider ) {
1119        $this->dbProvider = $dbProvider;
1120    }
1121
1122    /**
1123     * Begin a transaction on a DB
1124     *
1125     * This method makes it clear that begin() is called from a maintenance script,
1126     * which has outermost scope. This is safe, unlike $dbw->begin() called in other places.
1127     *
1128     * @param IDatabase $dbw
1129     * @param string $fname Caller name
1130     * @since 1.27
1131     */
1132    protected function beginTransaction( IDatabase $dbw, $fname ) {
1133        $dbw->begin( $fname );
1134    }
1135
1136    /**
1137     * Commit the transaction on a DB handle and wait for replica DBs to catch up
1138     *
1139     * This method makes it clear that commit() is called from a maintenance script,
1140     * which has outermost scope. This is safe, unlike $dbw->commit() called in other places.
1141     *
1142     * @param IDatabase $dbw
1143     * @param string $fname Caller name
1144     * @return bool Whether the replica DB wait succeeded
1145     * @since 1.27
1146     */
1147    protected function commitTransaction( IDatabase $dbw, $fname ) {
1148        $dbw->commit( $fname );
1149        return $this->waitForReplication();
1150    }
1151
1152    /**
1153     * Wait for replica DBs to catch up.
1154     *
1155     * @note Since 1.39, this also calls LBFactory::autoReconfigure().
1156     *
1157     * @return bool Whether the replica DB wait succeeded
1158     * @since 1.36
1159     */
1160    protected function waitForReplication() {
1161        $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
1162        $waitSucceeded = $lbFactory->waitForReplication(
1163            [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1164        );
1165        $this->lastReplicationWait = microtime( true );
1166
1167        // If possible, apply changes to the database configuration.
1168        // The primary use case for this is taking replicas out of rotation.
1169        // Long-running scripts may otherwise keep connections to
1170        // de-pooled database hosts, and may even re-connect to them.
1171        // If no config callback was configured, this has no effect.
1172        $lbFactory->autoReconfigure();
1173
1174        return $waitSucceeded;
1175    }
1176
1177    /**
1178     * Rollback the transaction on a DB handle
1179     *
1180     * This method makes it clear that rollback() is called from a maintenance script,
1181     * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1182     *
1183     * @param IDatabase $dbw
1184     * @param string $fname Caller name
1185     * @since 1.27
1186     */
1187    protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1188        $dbw->rollback( $fname );
1189    }
1190
1191    /**
1192     * Count down from $seconds to zero on the terminal, with a one-second pause
1193     * between showing each number. If the maintenance script is in quiet mode,
1194     * this function does nothing.
1195     *
1196     * @since 1.31
1197     *
1198     * @codeCoverageIgnore
1199     * @param int $seconds
1200     */
1201    protected function countDown( $seconds ) {
1202        if ( $this->isQuiet() ) {
1203            return;
1204        }
1205        for ( $i = $seconds; $i >= 0; $i-- ) {
1206            if ( $i != $seconds ) {
1207                $this->output( str_repeat( "\x08", strlen( (string)( $i + 1 ) ) ) );
1208            }
1209            $this->output( (string)$i );
1210            if ( $i ) {
1211                sleep( 1 );
1212            }
1213        }
1214        $this->output( "\n" );
1215    }
1216
1217    /**
1218     * Wrapper for posix_isatty()
1219     * We default as considering stdin a tty (for nice readline methods)
1220     * but treating stout as not a tty to avoid color codes
1221     *
1222     * @param mixed $fd File descriptor
1223     * @return bool
1224     */
1225    public static function posix_isatty( $fd ) {
1226        if ( !function_exists( 'posix_isatty' ) ) {
1227            return !$fd;
1228        }
1229
1230        return posix_isatty( $fd );
1231    }
1232
1233    /**
1234     * Prompt the console for input
1235     * @param string $prompt What to begin the line with, like '> '
1236     * @return string|false Response
1237     */
1238    public static function readconsole( $prompt = '> ' ) {
1239        static $isatty = null;
1240        if ( $isatty === null ) {
1241            $isatty = self::posix_isatty( 0 /*STDIN*/ );
1242        }
1243
1244        if ( $isatty && function_exists( 'readline' ) ) {
1245            return readline( $prompt );
1246        }
1247
1248        if ( $isatty ) {
1249            $st = self::readlineEmulation( $prompt );
1250        } elseif ( feof( STDIN ) ) {
1251            $st = false;
1252        } else {
1253            $st = fgets( STDIN, 1024 );
1254        }
1255        if ( $st === false ) {
1256            return false;
1257        }
1258
1259        return trim( $st );
1260    }
1261
1262    /**
1263     * Emulate readline()
1264     * @param string $prompt What to begin the line with, like '> '
1265     * @return string|false
1266     */
1267    private static function readlineEmulation( $prompt ) {
1268        $bash = ExecutableFinder::findInDefaultPaths( 'bash' );
1269        if ( !wfIsWindows() && $bash ) {
1270            $encPrompt = Shell::escape( $prompt );
1271            $command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1272            $result = Shell::command( $bash, '-c', $command )
1273                ->passStdin()
1274                ->forwardStderr()
1275                ->execute();
1276
1277            if ( $result->getExitCode() == 0 ) {
1278                return $result->getStdout();
1279            }
1280
1281            if ( $result->getExitCode() == 127 ) {
1282                // Couldn't execute bash even though we thought we saw it.
1283                // Shell probably spit out an error message, sorry :(
1284                // Fall through to fgets()...
1285            } else {
1286                // EOF/ctrl+D
1287                return false;
1288            }
1289        }
1290
1291        // Fallback... we'll have no editing controls, EWWW
1292        if ( feof( STDIN ) ) {
1293            return false;
1294        }
1295        print $prompt;
1296
1297        return fgets( STDIN, 1024 );
1298    }
1299
1300    /**
1301     * Get the terminal size as a two-element array where the first element
1302     * is the width (number of columns) and the second element is the height
1303     * (number of rows).
1304     *
1305     * @return array
1306     */
1307    public static function getTermSize() {
1308        static $termSize = null;
1309
1310        if ( $termSize !== null ) {
1311            return $termSize;
1312        }
1313
1314        $default = [ 80, 50 ];
1315
1316        if ( wfIsWindows() || Shell::isDisabled() ) {
1317            $termSize = $default;
1318
1319            return $termSize;
1320        }
1321
1322        // It's possible to get the screen size with VT-100 terminal escapes,
1323        // but reading the responses is not possible without setting raw mode
1324        // (unless you want to require the user to press enter), and that
1325        // requires an ioctl(), which we can't do. So we have to shell out to
1326        // something that can do the relevant syscalls. There are a few
1327        // options. Linux and Mac OS X both have "stty size" which does the
1328        // job directly.
1329        $result = Shell::command( 'stty', 'size' )->passStdin()->execute();
1330        if ( $result->getExitCode() !== 0 ||
1331            !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m )
1332        ) {
1333            $termSize = $default;
1334
1335            return $termSize;
1336        }
1337
1338        $termSize = [ intval( $m[2] ), intval( $m[1] ) ];
1339
1340        return $termSize;
1341    }
1342
1343    /**
1344     * Call this to set up the autoloader to allow classes to be used from the
1345     * tests directory.
1346     *
1347     * @deprecated since 1.41. Set the MW_AUTOLOAD_TEST_CLASSES in file scope instead.
1348     */
1349    public static function requireTestsAutoloader() {
1350        require_once __DIR__ . '/../../tests/common/TestsAutoLoader.php';
1351    }
1352
1353    /**
1354     * Get a HookContainer, for running extension hooks or for hook metadata.
1355     *
1356     * @since 1.35
1357     * @return HookContainer
1358     */
1359    protected function getHookContainer() {
1360        if ( !$this->hookContainer ) {
1361            $this->hookContainer = $this->getServiceContainer()->getHookContainer();
1362        }
1363        return $this->hookContainer;
1364    }
1365
1366    /**
1367     * Get a HookRunner for running core hooks.
1368     *
1369     * @internal This is for use by core only. Hook interfaces may be removed
1370     *   without notice.
1371     * @since 1.35
1372     * @return HookRunner
1373     */
1374    protected function getHookRunner() {
1375        if ( !$this->hookRunner ) {
1376            $this->hookRunner = new HookRunner( $this->getHookContainer() );
1377        }
1378        return $this->hookRunner;
1379    }
1380
1381    /**
1382     * Utility function to parse a string (perhaps from a command line option)
1383     * into a list of integers (perhaps some kind of numeric IDs).
1384     *
1385     * @since 1.35
1386     *
1387     * @param string $text
1388     *
1389     * @return int[]
1390     */
1391    protected function parseIntList( $text ) {
1392        $ids = preg_split( '/[\s,;:|]+/', $text );
1393        $ids = array_map(
1394            static function ( $id ) {
1395                return (int)$id;
1396            },
1397            $ids
1398        );
1399        return array_filter( $ids );
1400    }
1401
1402    /**
1403     * @param string $errorMsg Error message to be displayed if the passed --user or --userid
1404     *  does not result in a valid existing user object.
1405     *
1406     * @since 1.37
1407     *
1408     * @return User
1409     */
1410    protected function validateUserOption( $errorMsg ) {
1411        if ( $this->hasOption( "user" ) ) {
1412            $user = User::newFromName( $this->getOption( 'user' ) );
1413        } elseif ( $this->hasOption( "userid" ) ) {
1414            $user = User::newFromId( $this->getOption( 'userid' ) );
1415        } else {
1416            $this->fatalError( $errorMsg );
1417        }
1418        if ( !$user || !$user->isRegistered() ) {
1419            if ( $this->hasOption( "user" ) ) {
1420                $this->fatalError( "No such user: " . $this->getOption( 'user' ) );
1421            } elseif ( $this->hasOption( "userid" ) ) {
1422                $this->fatalError( "No such user id: " . $this->getOption( 'userid' ) );
1423            }
1424        }
1425
1426        return $user;
1427    }
1428}