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