Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
49.88% |
203 / 407 |
|
56.76% |
42 / 74 |
CRAP | |
0.00% |
0 / 1 |
Maintenance | |
50.12% |
203 / 405 |
|
56.76% |
42 / 74 |
4109.24 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getParameters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
canExecuteWithoutLocalSettings | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supportsOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addOption | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
hasOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
deleteOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setAllowUnregisteredOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addDescription | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getArgs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getArgName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBatchSize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setBatchSize | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStdin | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
isQuiet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
output | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
5.12 | |||
error | |
33.33% |
6 / 18 |
|
0.00% |
0 / 1 |
26.96 | |||
fatalError | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
cleanupChanneled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
outputChanneled | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getDbType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addDefaultParams | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
getConfig | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getServiceContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
requireExtension | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkRequiredExtensions | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
runChild | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createChild | |
25.00% |
4 / 16 |
|
0.00% |
0 / 1 |
21.19 | |||
setup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
memoryLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
clearParamsAndArgs | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
loadWithArgv | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
3.79 | |||
loadParamsAndArgs | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
validateParamsAndArgs | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadSpecialVars | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
8.12 | |||
maybeHelp | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
6.97 | |||
showHelp | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
finalSetup | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
240 | |||
afterFinalSetup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
purgeRedundantText | |
70.00% |
28 / 40 |
|
0.00% |
0 / 1 |
8.32 | |||
getDir | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDB | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setDB | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReplicaDB | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPrimaryDB | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLBFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLBFactory | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
beginTransaction | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
commitTransaction | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
rollbackTransaction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
waitForReplication | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
beginTransactionRound | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
commitTransactionRound | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
rollbackTransactionRound | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
newBatchIterator | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
countDown | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
posix_isatty | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
readconsole | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
readlineEmulation | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
getTermSize | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
requireTestsAutoloader | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHookContainer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getHookRunner | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
parseIntList | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
validateUserOption | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
7 | |||
prompt | |
0.00% |
0 / 8 |
|
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 | |
21 | namespace MediaWiki\Maintenance; |
22 | |
23 | use Closure; |
24 | use DeferredUpdates; |
25 | use ExecutableFinder; |
26 | use Generator; |
27 | use MediaWiki; |
28 | use MediaWiki\Config\Config; |
29 | use MediaWiki\Debug\MWDebug; |
30 | use MediaWiki\HookContainer\HookContainer; |
31 | use MediaWiki\HookContainer\HookRunner; |
32 | use MediaWiki\MainConfigNames; |
33 | use MediaWiki\MediaWikiEntryPoint; |
34 | use MediaWiki\MediaWikiServices; |
35 | use MediaWiki\Registration\ExtensionRegistry; |
36 | use MediaWiki\Settings\SettingsBuilder; |
37 | use MediaWiki\Shell\Shell; |
38 | use MediaWiki\User\User; |
39 | use StatusValue; |
40 | use Wikimedia\Rdbms\IDatabase; |
41 | use Wikimedia\Rdbms\ILBFactory; |
42 | use Wikimedia\Rdbms\IMaintainableDatabase; |
43 | use Wikimedia\Rdbms\IReadableDatabase; |
44 | |
45 | // NOTE: MaintenanceParameters is needed in the constructor, and we may not have |
46 | // autoloading enabled at this point? |
47 | require_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 | */ |
78 | abstract 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 |