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