Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
49.63% |
201 / 405 |
|
56.76% |
42 / 74 |
CRAP | |
0.00% |
0 / 1 |
Maintenance | |
49.88% |
201 / 403 |
|
56.76% |
42 / 74 |
4168.06 | |
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 | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
5.15 | |||
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% |
13 / 13 |
|
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 | if ( $stats->getDataCount() > 1000 ) { |
499 | MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() ); |
500 | } |
501 | } |
502 | |
503 | if ( $this->mQuiet ) { |
504 | return; |
505 | } |
506 | if ( $channel === null ) { |
507 | $this->cleanupChanneled(); |
508 | print $out; |
509 | } else { |
510 | $out = preg_replace( '/\n\z/', '', $out ); |
511 | $this->outputChanneled( $out, $channel ); |
512 | } |
513 | } |
514 | |
515 | /** |
516 | * Throw an error to the user. Doesn't respect --quiet, so don't use |
517 | * this for non-error output |
518 | * @stable to override |
519 | * @param string|StatusValue $err The error to display |
520 | * @param int $die Deprecated since 1.31, use Maintenance::fatalError() instead |
521 | */ |
522 | protected function error( $err, $die = 0 ) { |
523 | if ( intval( $die ) !== 0 ) { |
524 | wfDeprecated( __METHOD__ . '( $err, $die )', '1.31' ); |
525 | $this->fatalError( $err, intval( $die ) ); |
526 | } |
527 | if ( $err instanceof StatusValue ) { |
528 | foreach ( [ 'warning' => 'Warning: ', 'error' => 'Error: ' ] as $type => $prefix ) { |
529 | foreach ( $err->getMessages( $type ) as $msg ) { |
530 | $this->error( |
531 | $prefix . wfMessage( $msg ) |
532 | ->inLanguage( 'en' ) |
533 | ->useDatabase( false ) |
534 | ->text() |
535 | ); |
536 | } |
537 | } |
538 | return; |
539 | } |
540 | $this->outputChanneled( false ); |
541 | if ( |
542 | ( PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg' ) && |
543 | !defined( 'MW_PHPUNIT_TEST' ) |
544 | ) { |
545 | fwrite( STDERR, $err . "\n" ); |
546 | } else { |
547 | print $err . "\n"; |
548 | } |
549 | } |
550 | |
551 | /** |
552 | * Output a message and terminate the current script. |
553 | * |
554 | * @stable to override |
555 | * @param string|StatusValue $msg Error message |
556 | * @param int $exitCode PHP exit status. Should be in range 1-254. |
557 | * @since 1.31 |
558 | * @return never |
559 | */ |
560 | protected function fatalError( $msg, $exitCode = 1 ) { |
561 | $this->error( $msg ); |
562 | // If running PHPUnit tests we don't want to call exit, as it will end the test suite early. |
563 | // Instead, throw an exception that will still cause the relevant test to fail if the ::fatalError |
564 | // call was not expected. |
565 | if ( defined( 'MW_PHPUNIT_TEST' ) ) { |
566 | throw new MaintenanceFatalError( $exitCode ); |
567 | } else { |
568 | exit( $exitCode ); |
569 | } |
570 | } |
571 | |
572 | /** @var bool */ |
573 | private $atLineStart = true; |
574 | /** @var string|null */ |
575 | private $lastChannel = null; |
576 | |
577 | /** |
578 | * Clean up channeled output. Output a newline if necessary. |
579 | */ |
580 | public function cleanupChanneled() { |
581 | if ( !$this->atLineStart ) { |
582 | print "\n"; |
583 | $this->atLineStart = true; |
584 | } |
585 | } |
586 | |
587 | /** |
588 | * Message outputter with channeled message support. Messages on the |
589 | * same channel are concatenated, but any intervening messages in another |
590 | * channel start a new line. |
591 | * @param string|false $msg The message without trailing newline |
592 | * @param string|null $channel Channel identifier or null for no |
593 | * channel. Channel comparison uses ===. |
594 | */ |
595 | public function outputChanneled( $msg, $channel = null ) { |
596 | if ( $msg === false ) { |
597 | $this->cleanupChanneled(); |
598 | |
599 | return; |
600 | } |
601 | |
602 | // End the current line if necessary |
603 | if ( !$this->atLineStart && $channel !== $this->lastChannel ) { |
604 | print "\n"; |
605 | } |
606 | |
607 | print $msg; |
608 | |
609 | $this->atLineStart = false; |
610 | if ( $channel === null ) { |
611 | // For unchanneled messages, output trailing newline immediately |
612 | print "\n"; |
613 | $this->atLineStart = true; |
614 | } |
615 | $this->lastChannel = $channel; |
616 | } |
617 | |
618 | /** |
619 | * Does the script need different DB access? By default, we give Maintenance |
620 | * scripts normal rights to the DB. Sometimes, a script needs admin rights |
621 | * access for a reason and sometimes they want no access. Subclasses should |
622 | * override and return one of the following values, as needed: |
623 | * Maintenance::DB_NONE - For no DB access at all |
624 | * Maintenance::DB_STD - For normal DB access, default |
625 | * Maintenance::DB_ADMIN - For admin DB access |
626 | * |
627 | * @note Subclasses that override this method to return self::DB_NONE should |
628 | * also override canExecuteWithoutLocalSettings() to return true, unless they |
629 | * need the wiki to be set up for reasons beyond access to a database connection. |
630 | * |
631 | * @see canExecuteWithoutLocalSettings() |
632 | * @stable to override |
633 | * @return int |
634 | */ |
635 | public function getDbType() { |
636 | return self::DB_STD; |
637 | } |
638 | |
639 | /** |
640 | * Add the default parameters to the scripts |
641 | */ |
642 | protected function addDefaultParams() { |
643 | # Generic (non-script-dependent) options: |
644 | |
645 | $this->addOption( 'help', 'Display this help message', false, false, 'h' ); |
646 | $this->addOption( 'quiet', 'Whether to suppress non-error output', false, false, 'q' ); |
647 | |
648 | # Save generic options to display them separately in help |
649 | $generic = [ 'help', 'quiet' ]; |
650 | $this->parameters->assignGroup( self::GENERIC_MAINTENANCE_PARAMETERS, $generic ); |
651 | |
652 | # Script-dependent options: |
653 | |
654 | // If we support a DB, show the options |
655 | if ( $this->getDbType() > 0 ) { |
656 | $this->addOption( 'dbuser', 'The DB user to use for this script', false, true ); |
657 | $this->addOption( 'dbpass', 'The password to use for this script', false, true ); |
658 | $this->addOption( 'dbgroupdefault', 'The default DB group to use.', false, true ); |
659 | } |
660 | |
661 | # Save additional script-dependent options to display |
662 | # them separately in help |
663 | $dependent = array_diff( |
664 | $this->parameters->getOptionNames(), |
665 | $generic |
666 | ); |
667 | $this->parameters->assignGroup( self::SCRIPT_DEPENDENT_PARAMETERS, $dependent ); |
668 | } |
669 | |
670 | /** |
671 | * @since 1.24 |
672 | * @stable to override |
673 | * @return Config |
674 | */ |
675 | public function getConfig() { |
676 | if ( $this->config === null ) { |
677 | $this->config = $this->getServiceContainer()->getMainConfig(); |
678 | } |
679 | |
680 | return $this->config; |
681 | } |
682 | |
683 | /** |
684 | * Returns the main service container. |
685 | * |
686 | * @since 1.40 |
687 | * @return MediaWikiServices |
688 | */ |
689 | protected function getServiceContainer() { |
690 | return MediaWikiServices::getInstance(); |
691 | } |
692 | |
693 | /** |
694 | * @since 1.24 |
695 | * @param Config $config |
696 | */ |
697 | public function setConfig( Config $config ) { |
698 | $this->config = $config; |
699 | } |
700 | |
701 | /** |
702 | * Indicate that the specified extension must be |
703 | * loaded before the script can run. |
704 | * |
705 | * This *must* be called in the constructor. |
706 | * |
707 | * @since 1.28 |
708 | * @param string $name |
709 | */ |
710 | protected function requireExtension( $name ) { |
711 | $this->requiredExtensions[] = $name; |
712 | } |
713 | |
714 | /** |
715 | * Verify that the required extensions are installed |
716 | * |
717 | * @since 1.28 |
718 | */ |
719 | public function checkRequiredExtensions() { |
720 | $registry = ExtensionRegistry::getInstance(); |
721 | $missing = []; |
722 | foreach ( $this->requiredExtensions as $name ) { |
723 | if ( !$registry->isLoaded( $name ) ) { |
724 | $missing[] = $name; |
725 | } |
726 | } |
727 | |
728 | if ( $missing ) { |
729 | if ( count( $missing ) === 1 ) { |
730 | $msg = 'The "' . $missing[ 0 ] . '" extension must be installed for this script to run. ' |
731 | . 'Please enable it and then try again.'; |
732 | } else { |
733 | $msg = 'The following extensions must be installed for this script to run: "' |
734 | . implode( '", "', $missing ) . '". Please enable them and then try again.'; |
735 | } |
736 | $this->fatalError( $msg ); |
737 | } |
738 | } |
739 | |
740 | /** |
741 | * Returns an instance of the given maintenance script, with all of the current arguments |
742 | * passed to it. |
743 | * |
744 | * Callers are expected to run the returned maintenance script instance by calling {@link Maintenance::execute} |
745 | * |
746 | * @deprecated Since 1.43. Use {@link Maintenance::createChild} instead. This method is an alias to that method. |
747 | * @param string $maintClass A name of a child maintenance class |
748 | * @param string|null $classFile Full path of where the child is |
749 | * @return Maintenance The created instance, which the caller is expected to run by calling |
750 | * {@link Maintenance::execute} on the returned object. |
751 | */ |
752 | public function runChild( $maintClass, $classFile = null ) { |
753 | MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'runChild', '1.43' ); |
754 | return self::createChild( $maintClass, $classFile ); |
755 | } |
756 | |
757 | /** |
758 | * Returns an instance of the given maintenance script, with all of the current arguments |
759 | * passed to it. |
760 | * |
761 | * Callers are expected to run the returned maintenance script instance by calling {@link Maintenance::execute} |
762 | * |
763 | * @param string $maintClass A name of a child maintenance class |
764 | * @param string|null $classFile Full path of where the child is |
765 | * @stable to override |
766 | * @return Maintenance The created instance, which the caller is expected to run by calling |
767 | * {@link Maintenance::execute} on the returned object. |
768 | */ |
769 | public function createChild( string $maintClass, ?string $classFile = null ): Maintenance { |
770 | // Make sure the class is loaded first |
771 | if ( !class_exists( $maintClass ) ) { |
772 | if ( $classFile ) { |
773 | require_once $classFile; |
774 | } |
775 | if ( !class_exists( $maintClass ) ) { |
776 | $this->fatalError( "Cannot spawn child: $maintClass" ); |
777 | } |
778 | } |
779 | |
780 | /** |
781 | * @var Maintenance $child |
782 | */ |
783 | $child = new $maintClass(); |
784 | $child->loadParamsAndArgs( |
785 | $this->mSelf, |
786 | $this->parameters->getOptions(), |
787 | $this->parameters->getArgs() |
788 | ); |
789 | if ( $this->mDb !== null ) { |
790 | $child->setDB( $this->mDb ); |
791 | } |
792 | if ( $this->lbFactory !== null ) { |
793 | $child->setLBFactory( $this->lbFactory ); |
794 | } |
795 | |
796 | return $child; |
797 | } |
798 | |
799 | /** |
800 | * Provides subclasses with an opportunity to perform initial checks. |
801 | * @stable to override |
802 | */ |
803 | public function setup() { |
804 | // noop |
805 | } |
806 | |
807 | /** |
808 | * Normally we disable the memory_limit when running admin scripts. |
809 | * Some scripts may wish to actually set a limit, however, to avoid |
810 | * blowing up unexpectedly. |
811 | * @stable to override |
812 | * @return string |
813 | */ |
814 | public function memoryLimit() { |
815 | return 'max'; |
816 | } |
817 | |
818 | /** |
819 | * Clear all params and arguments. |
820 | */ |
821 | public function clearParamsAndArgs() { |
822 | $this->parameters->clear(); |
823 | $this->mInputLoaded = false; |
824 | } |
825 | |
826 | /** |
827 | * @since 1.40 |
828 | * @internal |
829 | * @param string $name |
830 | */ |
831 | public function setName( string $name ) { |
832 | $this->mSelf = $name; |
833 | $this->parameters->setName( $this->mSelf ); |
834 | } |
835 | |
836 | /** |
837 | * Load params and arguments from a given array |
838 | * of command-line arguments |
839 | * |
840 | * @since 1.27 |
841 | * @param array $argv The argument array, not including the script itself. |
842 | */ |
843 | public function loadWithArgv( $argv ) { |
844 | if ( $this->mDescription ) { |
845 | $this->parameters->setDescription( $this->mDescription ); |
846 | } |
847 | |
848 | $this->parameters->loadWithArgv( $argv ); |
849 | |
850 | if ( $this->parameters->hasErrors() ) { |
851 | $errors = "\nERROR: " . implode( "\nERROR: ", $this->parameters->getErrors() ) . "\n"; |
852 | $this->error( $errors ); |
853 | $this->maybeHelp( true ); |
854 | } |
855 | |
856 | $this->loadSpecialVars(); |
857 | $this->mInputLoaded = true; |
858 | } |
859 | |
860 | /** |
861 | * Process command line arguments when running as a child script |
862 | * |
863 | * @param string|null $self The name of the script, if any |
864 | * @param array|null $opts An array of options, in form of key=>value |
865 | * @param array|null $args An array of command line arguments |
866 | */ |
867 | public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) { |
868 | # If we were given opts or args, set those and return early |
869 | if ( $self !== null || $opts !== null || $args !== null ) { |
870 | if ( $self !== null ) { |
871 | $this->mSelf = $self; |
872 | $this->parameters->setName( $self ); |
873 | } |
874 | $this->parameters->setOptionsAndArgs( $opts ?? [], $args ?? [] ); |
875 | $this->mInputLoaded = true; |
876 | } |
877 | |
878 | # If we've already loaded input (either by user values or from $argv) |
879 | # skip on loading it again. |
880 | if ( $this->mInputLoaded ) { |
881 | $this->loadSpecialVars(); |
882 | |
883 | return; |
884 | } |
885 | |
886 | global $argv; |
887 | $this->mSelf = $argv[0]; |
888 | $this->parameters->setName( $this->mSelf ); |
889 | $this->loadWithArgv( array_slice( $argv, 1 ) ); |
890 | } |
891 | |
892 | /** |
893 | * Run some validation checks on the params, etc |
894 | * @stable to override |
895 | */ |
896 | public function validateParamsAndArgs() { |
897 | $valid = $this->parameters->validate(); |
898 | |
899 | $this->maybeHelp( !$valid ); |
900 | } |
901 | |
902 | /** |
903 | * Handle the special variables that are global to all scripts |
904 | * @stable to override |
905 | */ |
906 | protected function loadSpecialVars() { |
907 | if ( $this->hasOption( 'dbuser' ) ) { |
908 | $this->mDbUser = $this->getOption( 'dbuser' ); |
909 | } |
910 | if ( $this->hasOption( 'dbpass' ) ) { |
911 | $this->mDbPass = $this->getOption( 'dbpass' ); |
912 | } |
913 | if ( $this->hasOption( 'quiet' ) ) { |
914 | $this->mQuiet = true; |
915 | } |
916 | if ( $this->hasOption( 'batch-size' ) ) { |
917 | $this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); |
918 | } |
919 | } |
920 | |
921 | /** |
922 | * Maybe show the help. If the help is shown, exit. |
923 | * |
924 | * @param bool $force Whether to force the help to show, default false |
925 | */ |
926 | protected function maybeHelp( $force = false ) { |
927 | if ( !$force && !$this->hasOption( 'help' ) ) { |
928 | return; |
929 | } |
930 | |
931 | if ( $this->parameters->hasErrors() && !$this->hasOption( 'help' ) ) { |
932 | $errors = "\nERROR: " . implode( "\nERROR: ", $this->parameters->getErrors() ) . "\n"; |
933 | $this->error( $errors ); |
934 | } |
935 | |
936 | $this->showHelp(); |
937 | $this->fatalError( '' ); |
938 | } |
939 | |
940 | /** |
941 | * Definitely show the help. Does not exit. |
942 | */ |
943 | protected function showHelp() { |
944 | $this->mQuiet = false; |
945 | $help = $this->parameters->getHelp(); |
946 | $this->output( $help ); |
947 | } |
948 | |
949 | /** |
950 | * Handle some last-minute setup here. |
951 | * |
952 | * @stable to override |
953 | * |
954 | * @param SettingsBuilder $settingsBuilder |
955 | */ |
956 | public function finalSetup( SettingsBuilder $settingsBuilder ) { |
957 | $config = $settingsBuilder->getConfig(); |
958 | $overrides = []; |
959 | $overrides['DBadminuser'] = $config->get( MainConfigNames::DBadminuser ); |
960 | $overrides['DBadminpassword'] = $config->get( MainConfigNames::DBadminpassword ); |
961 | |
962 | # Turn off output buffering again, it might have been turned on in the settings files |
963 | if ( ob_get_level() ) { |
964 | ob_end_flush(); |
965 | } |
966 | |
967 | # Override $wgServer |
968 | if ( $this->hasOption( 'server' ) ) { |
969 | $overrides['Server'] = $this->getOption( 'server', $config->get( MainConfigNames::Server ) ); |
970 | } |
971 | |
972 | # If these were passed, use them |
973 | if ( $this->mDbUser ) { |
974 | $overrides['DBadminuser'] = $this->mDbUser; |
975 | } |
976 | if ( $this->mDbPass ) { |
977 | $overrides['DBadminpassword'] = $this->mDbPass; |
978 | } |
979 | if ( $this->hasOption( 'dbgroupdefault' ) ) { |
980 | $overrides['DBDefaultGroup'] = $this->getOption( 'dbgroupdefault', null ); |
981 | // TODO: once MediaWikiServices::getInstance() starts throwing exceptions |
982 | // and not deprecation warnings for premature access to service container, |
983 | // we can remove this line. This method is called before Setup.php, |
984 | // so it would be guaranteed DBLoadBalancerFactory is not yet initialized. |
985 | if ( MediaWikiServices::hasInstance() ) { |
986 | $service = $this->getServiceContainer()->peekService( 'DBLoadBalancerFactory' ); |
987 | if ( $service ) { |
988 | $service->destroy(); |
989 | } |
990 | } |
991 | } |
992 | |
993 | if ( $this->getDbType() == self::DB_ADMIN && isset( $overrides[ 'DBadminuser' ] ) ) { |
994 | $overrides['DBuser'] = $overrides[ 'DBadminuser' ]; |
995 | $overrides['DBpassword'] = $overrides[ 'DBadminpassword' ]; |
996 | |
997 | /** @var array $dbServers */ |
998 | $dbServers = $config->get( MainConfigNames::DBservers ); |
999 | if ( $dbServers ) { |
1000 | foreach ( $dbServers as $i => $server ) { |
1001 | $dbServers[$i]['user'] = $overrides['DBuser']; |
1002 | $dbServers[$i]['password'] = $overrides['DBpassword']; |
1003 | } |
1004 | $overrides['DBservers'] = $dbServers; |
1005 | } |
1006 | |
1007 | $lbFactoryConf = $config->get( MainConfigNames::LBFactoryConf ); |
1008 | if ( isset( $lbFactoryConf['serverTemplate'] ) ) { |
1009 | $lbFactoryConf['serverTemplate']['user'] = $overrides['DBuser']; |
1010 | $lbFactoryConf['serverTemplate']['password'] = $overrides['DBpassword']; |
1011 | $overrides['LBFactoryConf'] = $lbFactoryConf; |
1012 | } |
1013 | |
1014 | // TODO: once MediaWikiServices::getInstance() starts throwing exceptions |
1015 | // and not deprecation warnings for premature access to service container, |
1016 | // we can remove this line. This method is called before Setup.php, |
1017 | // so it would be guaranteed DBLoadBalancerFactory is not yet initialized. |
1018 | if ( MediaWikiServices::hasInstance() ) { |
1019 | $service = $this->getServiceContainer()->peekService( 'DBLoadBalancerFactory' ); |
1020 | if ( $service ) { |
1021 | $service->destroy(); |
1022 | } |
1023 | } |
1024 | } |
1025 | |
1026 | $this->afterFinalSetup(); |
1027 | |
1028 | $overrides['ShowExceptionDetails'] = true; |
1029 | $overrides['ShowHostname'] = true; |
1030 | |
1031 | ini_set( 'max_execution_time', '0' ); |
1032 | $settingsBuilder->putConfigValues( $overrides ); |
1033 | } |
1034 | |
1035 | /** |
1036 | * Override to perform any required operation at the end of initialisation |
1037 | * @stable to override |
1038 | */ |
1039 | protected function afterFinalSetup() { |
1040 | } |
1041 | |
1042 | /** |
1043 | * Support function for cleaning up redundant text records |
1044 | * @param bool $delete Whether or not to actually delete the records |
1045 | * @author Rob Church <robchur@gmail.com> |
1046 | */ |
1047 | public function purgeRedundantText( $delete = true ) { |
1048 | # Data should come off the master, wrapped in a transaction |
1049 | $dbw = $this->getPrimaryDB(); |
1050 | $this->beginTransaction( $dbw, __METHOD__ ); |
1051 | |
1052 | # Get "active" text records via the content table |
1053 | $cur = []; |
1054 | $this->output( 'Searching for active text records via contents table...' ); |
1055 | $res = $dbw->newSelectQueryBuilder() |
1056 | ->select( 'content_address' ) |
1057 | ->distinct() |
1058 | ->from( 'content' ) |
1059 | ->caller( __METHOD__ )->fetchResultSet(); |
1060 | $blobStore = $this->getServiceContainer()->getBlobStore(); |
1061 | foreach ( $res as $row ) { |
1062 | // @phan-suppress-next-line PhanUndeclaredMethod |
1063 | $textId = $blobStore->getTextIdFromAddress( $row->content_address ); |
1064 | if ( $textId ) { |
1065 | $cur[] = $textId; |
1066 | } |
1067 | } |
1068 | $this->output( "done.\n" ); |
1069 | |
1070 | # Get the IDs of all text records not in these sets |
1071 | $this->output( 'Searching for inactive text records...' ); |
1072 | $textTableQueryBuilder = $dbw->newSelectQueryBuilder() |
1073 | ->select( 'old_id' ) |
1074 | ->distinct() |
1075 | ->from( 'text' ); |
1076 | if ( count( $cur ) ) { |
1077 | $textTableQueryBuilder->where( $dbw->expr( 'old_id', '!=', $cur ) ); |
1078 | } |
1079 | $res = $textTableQueryBuilder |
1080 | ->caller( __METHOD__ ) |
1081 | ->fetchResultSet(); |
1082 | $old = []; |
1083 | foreach ( $res as $row ) { |
1084 | $old[] = $row->old_id; |
1085 | } |
1086 | $this->output( "done.\n" ); |
1087 | |
1088 | # Inform the user of what we're going to do |
1089 | $count = count( $old ); |
1090 | $this->output( "$count inactive items found.\n" ); |
1091 | |
1092 | # Delete as appropriate |
1093 | if ( $delete && $count ) { |
1094 | $this->output( 'Deleting...' ); |
1095 | $dbw->newDeleteQueryBuilder() |
1096 | ->deleteFrom( 'text' ) |
1097 | ->where( [ 'old_id' => $old ] ) |
1098 | ->caller( __METHOD__ ) |
1099 | ->execute(); |
1100 | $this->output( "done.\n" ); |
1101 | } |
1102 | |
1103 | $this->commitTransaction( $dbw, __METHOD__ ); |
1104 | } |
1105 | |
1106 | /** |
1107 | * Get the maintenance directory. |
1108 | * @return string |
1109 | */ |
1110 | protected function getDir() { |
1111 | return __DIR__ . '/../'; |
1112 | } |
1113 | |
1114 | /** |
1115 | * Returns a database to be used by current maintenance script. |
1116 | * |
1117 | * This uses the main LBFactory instance by default unless overriden via setDB(). |
1118 | * |
1119 | * This function has the same parameters as LoadBalancer::getConnection(). |
1120 | * |
1121 | * For simple cases, use ::getReplicaDB() or ::getPrimaryDB() instead. |
1122 | * |
1123 | * @stable to override |
1124 | * |
1125 | * @param int $db DB index (DB_REPLICA/DB_PRIMARY) |
1126 | * @param string|string[] $groups default: empty array |
1127 | * @param string|bool $dbDomain default: current wiki |
1128 | * @return IMaintainableDatabase |
1129 | */ |
1130 | protected function getDB( $db, $groups = [], $dbDomain = false ) { |
1131 | if ( $this->mDb === null ) { |
1132 | return $this->getServiceContainer() |
1133 | ->getDBLoadBalancerFactory() |
1134 | ->getMainLB( $dbDomain ) |
1135 | ->getMaintenanceConnectionRef( $db, $groups, $dbDomain ); |
1136 | } |
1137 | |
1138 | return $this->mDb; |
1139 | } |
1140 | |
1141 | /** |
1142 | * Sets database object to be returned by getDB(). |
1143 | * @stable to override |
1144 | * |
1145 | * @param IMaintainableDatabase $db |
1146 | */ |
1147 | public function setDB( IMaintainableDatabase $db ) { |
1148 | $this->mDb = $db; |
1149 | } |
1150 | |
1151 | /** |
1152 | * @return IReadableDatabase |
1153 | * @since 1.42 |
1154 | */ |
1155 | protected function getReplicaDB(): IReadableDatabase { |
1156 | return $this->getLBFactory()->getReplicaDatabase(); |
1157 | } |
1158 | |
1159 | /** |
1160 | * @return IDatabase |
1161 | * @since 1.42 |
1162 | */ |
1163 | protected function getPrimaryDB(): IDatabase { |
1164 | return $this->getLBFactory()->getPrimaryDatabase(); |
1165 | } |
1166 | |
1167 | /** |
1168 | * @internal |
1169 | * @param ILBFactory $lbFactory LBFactory to inject in place of the service instance |
1170 | * @return void |
1171 | */ |
1172 | public function setLBFactory( ILBFactory $lbFactory ) { |
1173 | $this->lbFactory = $lbFactory; |
1174 | } |
1175 | |
1176 | /** |
1177 | * @return ILBFactory Injected LBFactory, if any, the service instance, otherwise |
1178 | */ |
1179 | private function getLBFactory() { |
1180 | $this->lbFactory ??= $this->getServiceContainer()->getDBLoadBalancerFactory(); |
1181 | |
1182 | return $this->lbFactory; |
1183 | } |
1184 | |
1185 | /** |
1186 | * Begin a transaction on a DB handle |
1187 | * |
1188 | * Maintenance scripts should call this method instead of {@link IDatabase::begin()}. |
1189 | * Use of this method makes it clear that the caller is a maintenance script, which has |
1190 | * the outermost transaction scope needed to explicitly begin transactions. |
1191 | * |
1192 | * This method makes it clear that begin() is called from a maintenance script, |
1193 | * which has outermost scope. This is safe, unlike $dbw->begin() called in other places. |
1194 | * |
1195 | * Use this method for scripts with direct, straightforward, control of all writes. |
1196 | * |
1197 | * @param IDatabase $dbw |
1198 | * @param string $fname Caller name |
1199 | * @since 1.27 |
1200 | * @deprecated Since 1.44 Use {@link beginTransactionRound()} instead |
1201 | */ |
1202 | protected function beginTransaction( IDatabase $dbw, $fname ) { |
1203 | $dbw->begin( $fname ); |
1204 | } |
1205 | |
1206 | /** |
1207 | * Commit the transaction on a DB handle and wait for replica DB servers to catch up |
1208 | * |
1209 | * Maintenance scripts should call this method instead of {@link IDatabase::commit()}. |
1210 | * Use of this method makes it clear that the caller is a maintenance script, which has |
1211 | * the outermost transaction scope needed to explicitly commit transactions. |
1212 | * |
1213 | * Use this method for scripts with direct, straightforward, control of all writes. |
1214 | * |
1215 | * @param IDatabase $dbw |
1216 | * @param string $fname Caller name |
1217 | * @return bool Whether the replication wait succeeded |
1218 | * @since 1.27 |
1219 | * @deprecated Since 1.44 Use {@link commitTransactionRound()} instead |
1220 | */ |
1221 | protected function commitTransaction( IDatabase $dbw, $fname ) { |
1222 | $dbw->commit( $fname ); |
1223 | return $this->waitForReplication(); |
1224 | } |
1225 | |
1226 | /** |
1227 | * Rollback the transaction on a DB handle |
1228 | * |
1229 | * Maintenance scripts should call this method instead of {@link IDatabase::rollback()}. |
1230 | * Use of this method makes it clear that the caller is a maintenance script, which has |
1231 | * the outermost transaction scope needed to explicitly roll back transactions. |
1232 | * |
1233 | * Use this method for scripts with direct, straightforward, control of all writes. |
1234 | * |
1235 | * @param IDatabase $dbw |
1236 | * @param string $fname Caller name |
1237 | * @since 1.27 |
1238 | * @deprecated Since 1.44 Use {@link rollbackTransactionRound()} instead |
1239 | */ |
1240 | protected function rollbackTransaction( IDatabase $dbw, $fname ) { |
1241 | $dbw->rollback( $fname ); |
1242 | } |
1243 | |
1244 | /** |
1245 | * Wait for replica DB servers to catch up |
1246 | * |
1247 | * Use this method after performing a batch of autocommit writes inscripts with direct, |
1248 | * straightforward, control of all writes. |
1249 | * |
1250 | * @note Since 1.39, this also calls LBFactory::autoReconfigure(). |
1251 | * |
1252 | * @return bool Whether the replication wait succeeded |
1253 | * @since 1.36 |
1254 | * @deprecated Since 1.44 Batch writes and use {@link commitTransactionRound()} instead |
1255 | */ |
1256 | protected function waitForReplication() { |
1257 | $lbFactory = $this->getLBFactory(); |
1258 | |
1259 | $waitSucceeded = $lbFactory->waitForReplication( |
1260 | [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] |
1261 | ); |
1262 | $this->lastReplicationWait = microtime( true ); |
1263 | |
1264 | // If possible, apply changes to the database configuration. |
1265 | // The primary use case for this is taking replicas out of rotation. |
1266 | // Long-running scripts may otherwise keep connections to |
1267 | // de-pooled database hosts, and may even re-connect to them. |
1268 | // If no config callback was configured, this has no effect. |
1269 | $lbFactory->autoReconfigure(); |
1270 | |
1271 | return $waitSucceeded; |
1272 | } |
1273 | |
1274 | /** |
1275 | * Start a transactional batch of DB operations |
1276 | * |
1277 | * Use this method for scripts that split up their work into logical transactions. |
1278 | * |
1279 | * This method is suitable even for scripts lacking direct, straightforward, control of |
1280 | * all writes. Such scripts might invoke complex methods of service objects, which might |
1281 | * easily touch multiple DB servers. This method proves the usual best-effort distributed |
1282 | * transactions that DBO_DEFAULT provides during web requests. |
1283 | * |
1284 | * @see ILBfactory::beginPrimaryChanges() |
1285 | * |
1286 | * @param string $fname Caller name |
1287 | * @return void |
1288 | * @since 1.44 |
1289 | */ |
1290 | protected function beginTransactionRound( $fname ) { |
1291 | $lbFactory = $this->getLBFactory(); |
1292 | |
1293 | $lbFactory->beginPrimaryChanges( $fname ); |
1294 | } |
1295 | |
1296 | /** |
1297 | * Commit a transactional batch of DB operations and wait for replica DB servers to catch up |
1298 | * |
1299 | * Use this method for scripts that split up their work into logical transactions. |
1300 | * |
1301 | * @see ILBfactory::commitPrimaryChanges() |
1302 | * |
1303 | * @param string $fname Caller name |
1304 | * @return bool Whether the replication wait succeeded |
1305 | * @since 1.44 |
1306 | */ |
1307 | protected function commitTransactionRound( $fname ) { |
1308 | $lbFactory = $this->getLBFactory(); |
1309 | |
1310 | $lbFactory->commitPrimaryChanges( $fname ); |
1311 | |
1312 | $waitSucceeded = $lbFactory->waitForReplication( |
1313 | [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] |
1314 | ); |
1315 | $this->lastReplicationWait = microtime( true ); |
1316 | |
1317 | // Periodically run any deferred updates that accumulate |
1318 | DeferredUpdates::tryOpportunisticExecute(); |
1319 | // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385) |
1320 | MediaWikiEntryPoint::emitBufferedStatsdData( |
1321 | $this->getServiceContainer()->getStatsdDataFactory(), |
1322 | $this->getConfig() |
1323 | ); |
1324 | |
1325 | // If possible, apply changes to the database configuration. |
1326 | // The primary use case for this is taking replicas out of rotation. |
1327 | // Long-running scripts may otherwise keep connections to |
1328 | // de-pooled database hosts, and may even re-connect to them. |
1329 | // If no config callback was configured, this has no effect. |
1330 | $lbFactory->autoReconfigure(); |
1331 | |
1332 | return $waitSucceeded; |
1333 | } |
1334 | |
1335 | /** |
1336 | * Rollback a transactional batch of DB operations |
1337 | * |
1338 | * Use this method for scripts that split up their work into logical transactions. |
1339 | * Note that this does not call {@link ILBfactory::flushPrimarySessions()}. |
1340 | * |
1341 | * @see ILBfactory::rollbackPrimaryChanges() |
1342 | * |
1343 | * @param string $fname Caller name |
1344 | * @return void |
1345 | * @since 1.44 |
1346 | */ |
1347 | protected function rollbackTransactionRound( $fname ) { |
1348 | $lbFactory = $this->getLBFactory(); |
1349 | |
1350 | $lbFactory->rollbackPrimaryChanges( $fname ); |
1351 | } |
1352 | |
1353 | /** |
1354 | * Wrap an entry iterator into a generator that returns batches of said entries |
1355 | * |
1356 | * The batch size is determined by {@link getBatchSize()}. |
1357 | * |
1358 | * @param iterable|Generator|Closure $source An iterable or a callback to get one |
1359 | * @return iterable<array> New iterable yielding entry batches from the given iterable |
1360 | * @since 1.44 |
1361 | */ |
1362 | final protected function newBatchIterator( $source ): iterable { |
1363 | $batchSize = max( $this->getBatchSize(), 1 ); |
1364 | if ( $source instanceof Closure ) { |
1365 | $iterable = $source(); |
1366 | } else { |
1367 | $iterable = $source; |
1368 | } |
1369 | |
1370 | $entryBatch = []; |
1371 | foreach ( $iterable as $key => $entry ) { |
1372 | $entryBatch[$key] = $entry; |
1373 | if ( count( $entryBatch ) >= $batchSize ) { |
1374 | yield $entryBatch; |
1375 | $entryBatch = []; |
1376 | } |
1377 | } |
1378 | if ( $entryBatch ) { |
1379 | yield $entryBatch; |
1380 | } |
1381 | } |
1382 | |
1383 | /** |
1384 | * Count down from $seconds to zero on the terminal, with a one-second pause |
1385 | * between showing each number. If the maintenance script is in quiet mode, |
1386 | * this function does nothing. |
1387 | * |
1388 | * @since 1.31 |
1389 | * |
1390 | * @codeCoverageIgnore |
1391 | * @param int $seconds |
1392 | */ |
1393 | protected function countDown( $seconds ) { |
1394 | if ( $this->isQuiet() ) { |
1395 | return; |
1396 | } |
1397 | for ( $i = $seconds; $i >= 0; $i-- ) { |
1398 | if ( $i != $seconds ) { |
1399 | $this->output( str_repeat( "\x08", strlen( (string)( $i + 1 ) ) ) ); |
1400 | } |
1401 | $this->output( (string)$i ); |
1402 | if ( $i ) { |
1403 | sleep( 1 ); |
1404 | } |
1405 | } |
1406 | $this->output( "\n" ); |
1407 | } |
1408 | |
1409 | /** |
1410 | * Wrapper for posix_isatty() |
1411 | * We default as considering stdin a tty (for nice readline methods) |
1412 | * but treating stout as not a tty to avoid color codes |
1413 | * |
1414 | * @param mixed $fd File descriptor |
1415 | * @return bool |
1416 | */ |
1417 | public static function posix_isatty( $fd ) { |
1418 | if ( !function_exists( 'posix_isatty' ) ) { |
1419 | return !$fd; |
1420 | } |
1421 | |
1422 | return posix_isatty( $fd ); |
1423 | } |
1424 | |
1425 | /** |
1426 | * Prompt the console for input |
1427 | * @param string $prompt What to begin the line with, like '> ' |
1428 | * @return string|false Response |
1429 | */ |
1430 | public static function readconsole( $prompt = '> ' ) { |
1431 | static $isatty = null; |
1432 | $isatty ??= self::posix_isatty( 0 /*STDIN*/ ); |
1433 | |
1434 | if ( $isatty && function_exists( 'readline' ) ) { |
1435 | return readline( $prompt ); |
1436 | } |
1437 | |
1438 | if ( $isatty ) { |
1439 | $st = self::readlineEmulation( $prompt ); |
1440 | } elseif ( feof( STDIN ) ) { |
1441 | $st = false; |
1442 | } else { |
1443 | $st = fgets( STDIN, 1024 ); |
1444 | } |
1445 | if ( $st === false ) { |
1446 | return false; |
1447 | } |
1448 | |
1449 | return trim( $st ); |
1450 | } |
1451 | |
1452 | /** |
1453 | * Emulate readline() |
1454 | * @param string $prompt What to begin the line with, like '> ' |
1455 | * @return string|false |
1456 | */ |
1457 | private static function readlineEmulation( $prompt ) { |
1458 | $bash = ExecutableFinder::findInDefaultPaths( 'bash' ); |
1459 | if ( !wfIsWindows() && $bash ) { |
1460 | $encPrompt = Shell::escape( $prompt ); |
1461 | $command = "read -er -p $encPrompt && echo \"\$REPLY\""; |
1462 | $result = Shell::command( $bash, '-c', $command ) |
1463 | ->passStdin() |
1464 | ->forwardStderr() |
1465 | ->execute(); |
1466 | |
1467 | if ( $result->getExitCode() == 0 ) { |
1468 | return $result->getStdout(); |
1469 | } |
1470 | |
1471 | if ( $result->getExitCode() == 127 ) { |
1472 | // Couldn't execute bash even though we thought we saw it. |
1473 | // Shell probably spit out an error message, sorry :( |
1474 | // Fall through to fgets()... |
1475 | } else { |
1476 | // EOF/ctrl+D |
1477 | return false; |
1478 | } |
1479 | } |
1480 | |
1481 | // Fallback... we'll have no editing controls, EWWW |
1482 | if ( feof( STDIN ) ) { |
1483 | return false; |
1484 | } |
1485 | print $prompt; |
1486 | |
1487 | return fgets( STDIN, 1024 ); |
1488 | } |
1489 | |
1490 | /** |
1491 | * Get the terminal size as a two-element array where the first element |
1492 | * is the width (number of columns) and the second element is the height |
1493 | * (number of rows). |
1494 | * |
1495 | * @return array |
1496 | */ |
1497 | public static function getTermSize() { |
1498 | static $termSize = null; |
1499 | |
1500 | if ( $termSize !== null ) { |
1501 | return $termSize; |
1502 | } |
1503 | |
1504 | $default = [ 80, 50 ]; |
1505 | |
1506 | if ( wfIsWindows() || Shell::isDisabled() ) { |
1507 | $termSize = $default; |
1508 | |
1509 | return $termSize; |
1510 | } |
1511 | |
1512 | // It's possible to get the screen size with VT-100 terminal escapes, |
1513 | // but reading the responses is not possible without setting raw mode |
1514 | // (unless you want to require the user to press enter), and that |
1515 | // requires an ioctl(), which we can't do. So we have to shell out to |
1516 | // something that can do the relevant syscalls. There are a few |
1517 | // options. Linux and Mac OS X both have "stty size" which does the |
1518 | // job directly. |
1519 | $result = Shell::command( 'stty', 'size' )->passStdin()->execute(); |
1520 | if ( $result->getExitCode() !== 0 || |
1521 | !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m ) |
1522 | ) { |
1523 | $termSize = $default; |
1524 | |
1525 | return $termSize; |
1526 | } |
1527 | |
1528 | $termSize = [ intval( $m[2] ), intval( $m[1] ) ]; |
1529 | |
1530 | return $termSize; |
1531 | } |
1532 | |
1533 | /** |
1534 | * Call this to set up the autoloader to allow classes to be used from the |
1535 | * tests directory. |
1536 | * |
1537 | * @deprecated since 1.41. Set the MW_AUTOLOAD_TEST_CLASSES in file scope instead. |
1538 | */ |
1539 | public static function requireTestsAutoloader() { |
1540 | require_once __DIR__ . '/../../tests/common/TestsAutoLoader.php'; |
1541 | } |
1542 | |
1543 | /** |
1544 | * Get a HookContainer, for running extension hooks or for hook metadata. |
1545 | * |
1546 | * @since 1.35 |
1547 | * @return HookContainer |
1548 | */ |
1549 | protected function getHookContainer() { |
1550 | if ( !$this->hookContainer ) { |
1551 | $this->hookContainer = $this->getServiceContainer()->getHookContainer(); |
1552 | } |
1553 | return $this->hookContainer; |
1554 | } |
1555 | |
1556 | /** |
1557 | * Get a HookRunner for running core hooks. |
1558 | * |
1559 | * @internal This is for use by core only. Hook interfaces may be removed |
1560 | * without notice. |
1561 | * @since 1.35 |
1562 | * @return HookRunner |
1563 | */ |
1564 | protected function getHookRunner() { |
1565 | if ( !$this->hookRunner ) { |
1566 | $this->hookRunner = new HookRunner( $this->getHookContainer() ); |
1567 | } |
1568 | return $this->hookRunner; |
1569 | } |
1570 | |
1571 | /** |
1572 | * Utility function to parse a string (perhaps from a command line option) |
1573 | * into a list of integers (perhaps some kind of numeric IDs). |
1574 | * |
1575 | * @since 1.35 |
1576 | * |
1577 | * @param string $text |
1578 | * |
1579 | * @return int[] |
1580 | */ |
1581 | protected function parseIntList( $text ) { |
1582 | $ids = preg_split( '/[\s,;:|]+/', $text ); |
1583 | $ids = array_map( |
1584 | static function ( $id ) { |
1585 | return (int)$id; |
1586 | }, |
1587 | $ids |
1588 | ); |
1589 | return array_filter( $ids ); |
1590 | } |
1591 | |
1592 | /** |
1593 | * @param string $errorMsg Error message to be displayed if neither --user or --userid are passed. |
1594 | * |
1595 | * @since 1.37 |
1596 | * |
1597 | * @return User |
1598 | */ |
1599 | protected function validateUserOption( $errorMsg ) { |
1600 | if ( $this->hasOption( "user" ) ) { |
1601 | $user = User::newFromName( $this->getOption( 'user' ) ); |
1602 | } elseif ( $this->hasOption( "userid" ) ) { |
1603 | $user = User::newFromId( $this->getOption( 'userid' ) ); |
1604 | } else { |
1605 | $this->fatalError( $errorMsg ); |
1606 | } |
1607 | if ( !$user || !$user->isRegistered() ) { |
1608 | if ( $this->hasOption( "user" ) ) { |
1609 | $this->fatalError( "No such user: " . $this->getOption( 'user' ) ); |
1610 | } elseif ( $this->hasOption( "userid" ) ) { |
1611 | $this->fatalError( "No such user id: " . $this->getOption( 'userid' ) ); |
1612 | } |
1613 | } |
1614 | |
1615 | return $user; |
1616 | } |
1617 | |
1618 | /** |
1619 | * @param string $prompt The prompt to display to the user |
1620 | * @param string|null $default The default value to return if the user just presses enter |
1621 | * |
1622 | * @return string|null |
1623 | * |
1624 | * @since 1.43 |
1625 | */ |
1626 | protected function prompt( string $prompt, ?string $default = null ): ?string { |
1627 | $defaultText = $default === null ? ' > ' : " [{$default}] > "; |
1628 | $promptWithDefault = $prompt . $defaultText; |
1629 | $line = self::readconsole( $promptWithDefault ); |
1630 | if ( $line === false ) { |
1631 | return $default; |
1632 | } |
1633 | if ( $line === '' ) { |
1634 | return $default; |
1635 | } |
1636 | |
1637 | return $line; |
1638 | } |
1639 | } |
1640 | |
1641 | /** @deprecated class alias since 1.43 */ |
1642 | class_alias( Maintenance::class, 'Maintenance' ); |