Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
50.61% |
209 / 413 |
|
56.76% |
42 / 74 |
CRAP | |
0.00% |
0 / 1 |
Maintenance | |
50.85% |
209 / 411 |
|
56.76% |
42 / 74 |
3939.57 | |
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% |
13 / 13 |
|
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 | $res = $textTableQueryBuilder |
1082 | ->caller( __METHOD__ ) |
1083 | ->fetchResultSet(); |
1084 | $old = []; |
1085 | foreach ( $res as $row ) { |
1086 | $old[] = $row->old_id; |
1087 | } |
1088 | $this->output( "done.\n" ); |
1089 | |
1090 | # Inform the user of what we're going to do |
1091 | $count = count( $old ); |
1092 | $this->output( "$count inactive items found.\n" ); |
1093 | |
1094 | # Delete as appropriate |
1095 | if ( $delete && $count ) { |
1096 | $this->output( 'Deleting...' ); |
1097 | $dbw->newDeleteQueryBuilder() |
1098 | ->deleteFrom( 'text' ) |
1099 | ->where( [ 'old_id' => $old ] ) |
1100 | ->caller( __METHOD__ ) |
1101 | ->execute(); |
1102 | $this->output( "done.\n" ); |
1103 | } |
1104 | |
1105 | $this->commitTransaction( $dbw, __METHOD__ ); |
1106 | } |
1107 | |
1108 | /** |
1109 | * Get the maintenance directory. |
1110 | * @return string |
1111 | */ |
1112 | protected function getDir() { |
1113 | return __DIR__ . '/../'; |
1114 | } |
1115 | |
1116 | /** |
1117 | * Returns a database to be used by current maintenance script. |
1118 | * |
1119 | * This uses the main LBFactory instance by default unless overriden via setDB(). |
1120 | * |
1121 | * This function has the same parameters as LoadBalancer::getConnection(). |
1122 | * |
1123 | * For simple cases, use ::getReplicaDB() or ::getPrimaryDB() instead. |
1124 | * |
1125 | * @stable to override |
1126 | * |
1127 | * @param int $db DB index (DB_REPLICA/DB_PRIMARY) |
1128 | * @param string|string[] $groups default: empty array |
1129 | * @param string|bool $dbDomain default: current wiki |
1130 | * @return IMaintainableDatabase |
1131 | */ |
1132 | protected function getDB( $db, $groups = [], $dbDomain = false ) { |
1133 | if ( $this->mDb === null ) { |
1134 | return $this->getServiceContainer() |
1135 | ->getDBLoadBalancerFactory() |
1136 | ->getMainLB( $dbDomain ) |
1137 | ->getMaintenanceConnectionRef( $db, $groups, $dbDomain ); |
1138 | } |
1139 | |
1140 | return $this->mDb; |
1141 | } |
1142 | |
1143 | /** |
1144 | * Sets database object to be returned by getDB(). |
1145 | * @stable to override |
1146 | * |
1147 | * @param IMaintainableDatabase $db |
1148 | */ |
1149 | public function setDB( IMaintainableDatabase $db ) { |
1150 | $this->mDb = $db; |
1151 | } |
1152 | |
1153 | /** |
1154 | * @return IReadableDatabase |
1155 | * @since 1.42 |
1156 | */ |
1157 | protected function getReplicaDB(): IReadableDatabase { |
1158 | return $this->getLBFactory()->getReplicaDatabase(); |
1159 | } |
1160 | |
1161 | /** |
1162 | * @return IDatabase |
1163 | * @since 1.42 |
1164 | */ |
1165 | protected function getPrimaryDB(): IDatabase { |
1166 | return $this->getLBFactory()->getPrimaryDatabase(); |
1167 | } |
1168 | |
1169 | /** |
1170 | * @internal |
1171 | * @param ILBFactory $lbFactory LBFactory to inject in place of the service instance |
1172 | * @return void |
1173 | */ |
1174 | public function setLBFactory( ILBFactory $lbFactory ) { |
1175 | $this->lbFactory = $lbFactory; |
1176 | } |
1177 | |
1178 | /** |
1179 | * @return ILBFactory Injected LBFactory, if any, the service instance, otherwise |
1180 | */ |
1181 | private function getLBFactory() { |
1182 | $this->lbFactory ??= $this->getServiceContainer()->getDBLoadBalancerFactory(); |
1183 | |
1184 | return $this->lbFactory; |
1185 | } |
1186 | |
1187 | /** |
1188 | * Begin a transaction on a DB handle |
1189 | * |
1190 | * Maintenance scripts should call this method instead of {@link IDatabase::begin()}. |
1191 | * Use of this method makes it clear that the caller is a maintenance script, which has |
1192 | * the outermost transaction scope needed to explicitly begin transactions. |
1193 | * |
1194 | * This method makes it clear that begin() is called from a maintenance script, |
1195 | * which has outermost scope. This is safe, unlike $dbw->begin() called in other places. |
1196 | * |
1197 | * Use this method for scripts with direct, straightforward, control of all writes. |
1198 | * |
1199 | * @param IDatabase $dbw |
1200 | * @param string $fname Caller name |
1201 | * @since 1.27 |
1202 | * @deprecated Since 1.44 Use {@link beginTransactionRound()} instead |
1203 | */ |
1204 | protected function beginTransaction( IDatabase $dbw, $fname ) { |
1205 | $dbw->begin( $fname ); |
1206 | } |
1207 | |
1208 | /** |
1209 | * Commit the transaction on a DB handle and wait for replica DB servers to catch up |
1210 | * |
1211 | * This method also triggers {@link DeferredUpdates::tryOpportunisticExecute()}. |
1212 | * |
1213 | * Maintenance scripts should call this method instead of {@link IDatabase::commit()}. |
1214 | * Use of this method makes it clear that the caller is a maintenance script, which has |
1215 | * the outermost transaction scope needed to explicitly commit transactions. |
1216 | * |
1217 | * Use this method for scripts with direct, straightforward, control of all writes. |
1218 | * |
1219 | * @param IDatabase $dbw |
1220 | * @param string $fname Caller name |
1221 | * @return bool Whether the replication wait succeeded |
1222 | * @since 1.27 |
1223 | * @deprecated Since 1.44 Use {@link commitTransactionRound()} instead |
1224 | */ |
1225 | protected function commitTransaction( IDatabase $dbw, $fname ) { |
1226 | $dbw->commit( $fname ); |
1227 | |
1228 | return $this->waitForReplication(); |
1229 | } |
1230 | |
1231 | /** |
1232 | * Rollback the transaction on a DB handle |
1233 | * |
1234 | * Maintenance scripts should call this method instead of {@link IDatabase::rollback()}. |
1235 | * Use of this method makes it clear that the caller is a maintenance script, which has |
1236 | * the outermost transaction scope needed to explicitly roll back transactions. |
1237 | * |
1238 | * Use this method for scripts with direct, straightforward, control of all writes. |
1239 | * |
1240 | * @param IDatabase $dbw |
1241 | * @param string $fname Caller name |
1242 | * @since 1.27 |
1243 | * @deprecated Since 1.44 Use {@link rollbackTransactionRound()} instead |
1244 | */ |
1245 | protected function rollbackTransaction( IDatabase $dbw, $fname ) { |
1246 | $dbw->rollback( $fname ); |
1247 | } |
1248 | |
1249 | /** |
1250 | * Wait for replica DB servers to catch up |
1251 | * |
1252 | * Use this method after performing a batch of autocommit writes inscripts with direct, |
1253 | * straightforward, control of all writes. |
1254 | * |
1255 | * @note Since 1.39, this also calls LBFactory::autoReconfigure(). |
1256 | * |
1257 | * @return bool Whether the replication wait succeeded |
1258 | * @since 1.36 |
1259 | * @deprecated Since 1.44 Batch writes and use {@link commitTransactionRound()} instead |
1260 | */ |
1261 | protected function waitForReplication() { |
1262 | $lbFactory = $this->getLBFactory(); |
1263 | |
1264 | $waitSucceeded = $lbFactory->waitForReplication( |
1265 | [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] |
1266 | ); |
1267 | $this->lastReplicationWait = microtime( true ); |
1268 | |
1269 | // If possible, apply changes to the database configuration. |
1270 | // The primary use case for this is taking replicas out of rotation. |
1271 | // Long-running scripts may otherwise keep connections to |
1272 | // de-pooled database hosts, and may even re-connect to them. |
1273 | // If no config callback was configured, this has no effect. |
1274 | $lbFactory->autoReconfigure(); |
1275 | |
1276 | // Periodically run any deferred updates that accumulate |
1277 | DeferredUpdates::tryOpportunisticExecute(); |
1278 | // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385) |
1279 | MediaWikiEntryPoint::emitBufferedStats( |
1280 | $this->getServiceContainer()->getStatsFactory(), |
1281 | $this->getServiceContainer()->getStatsdDataFactory(), |
1282 | $this->getConfig() |
1283 | ); |
1284 | |
1285 | return $waitSucceeded; |
1286 | } |
1287 | |
1288 | /** |
1289 | * Start a transactional batch of DB operations |
1290 | * |
1291 | * Use this method for scripts that split up their work into logical transactions. |
1292 | * |
1293 | * This method is suitable even for scripts lacking direct, straightforward, control of |
1294 | * all writes. Such scripts might invoke complex methods of service objects, which might |
1295 | * easily touch multiple DB servers. This method proves the usual best-effort distributed |
1296 | * transactions that DBO_DEFAULT provides during web requests. |
1297 | * |
1298 | * @see ILBfactory::beginPrimaryChanges() |
1299 | * |
1300 | * @param string $fname Caller name |
1301 | * @return void |
1302 | * @since 1.44 |
1303 | */ |
1304 | protected function beginTransactionRound( $fname ) { |
1305 | $lbFactory = $this->getLBFactory(); |
1306 | |
1307 | $lbFactory->beginPrimaryChanges( $fname ); |
1308 | } |
1309 | |
1310 | /** |
1311 | * Commit a transactional batch of DB operations and wait for replica DB servers to catch up |
1312 | * |
1313 | * Use this method for scripts that split up their work into logical transactions. |
1314 | * |
1315 | * This method also triggers {@link DeferredUpdates::tryOpportunisticExecute()}. |
1316 | * |
1317 | * @see ILBfactory::commitPrimaryChanges() |
1318 | * |
1319 | * @param string $fname Caller name |
1320 | * @return bool Whether the replication wait succeeded |
1321 | * @since 1.44 |
1322 | */ |
1323 | protected function commitTransactionRound( $fname ) { |
1324 | $lbFactory = $this->getLBFactory(); |
1325 | |
1326 | $lbFactory->commitPrimaryChanges( $fname ); |
1327 | |
1328 | $waitSucceeded = $lbFactory->waitForReplication( |
1329 | [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] |
1330 | ); |
1331 | $this->lastReplicationWait = microtime( true ); |
1332 | |
1333 | // Periodically run any deferred updates that accumulate |
1334 | DeferredUpdates::tryOpportunisticExecute(); |
1335 | // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385) |
1336 | MediaWikiEntryPoint::emitBufferedStats( |
1337 | $this->getServiceContainer()->getStatsFactory(), |
1338 | $this->getServiceContainer()->getStatsdDataFactory(), |
1339 | $this->getConfig() |
1340 | ); |
1341 | |
1342 | // If possible, apply changes to the database configuration. |
1343 | // The primary use case for this is taking replicas out of rotation. |
1344 | // Long-running scripts may otherwise keep connections to |
1345 | // de-pooled database hosts, and may even re-connect to them. |
1346 | // If no config callback was configured, this has no effect. |
1347 | $lbFactory->autoReconfigure(); |
1348 | |
1349 | return $waitSucceeded; |
1350 | } |
1351 | |
1352 | /** |
1353 | * Rollback a transactional batch of DB operations |
1354 | * |
1355 | * Use this method for scripts that split up their work into logical transactions. |
1356 | * Note that this does not call {@link ILBfactory::flushPrimarySessions()}. |
1357 | * |
1358 | * @see ILBfactory::rollbackPrimaryChanges() |
1359 | * |
1360 | * @param string $fname Caller name |
1361 | * @return void |
1362 | * @since 1.44 |
1363 | */ |
1364 | protected function rollbackTransactionRound( $fname ) { |
1365 | $lbFactory = $this->getLBFactory(); |
1366 | |
1367 | $lbFactory->rollbackPrimaryChanges( $fname ); |
1368 | } |
1369 | |
1370 | /** |
1371 | * Wrap an entry iterator into a generator that returns batches of said entries |
1372 | * |
1373 | * The batch size is determined by {@link getBatchSize()}. |
1374 | * |
1375 | * @param iterable|Generator|Closure $source An iterable or a callback to get one |
1376 | * @return iterable<array> New iterable yielding entry batches from the given iterable |
1377 | * @since 1.44 |
1378 | */ |
1379 | final protected function newBatchIterator( $source ): iterable { |
1380 | $batchSize = max( $this->getBatchSize(), 1 ); |
1381 | if ( $source instanceof Closure ) { |
1382 | $iterable = $source(); |
1383 | } else { |
1384 | $iterable = $source; |
1385 | } |
1386 | |
1387 | $entryBatch = []; |
1388 | foreach ( $iterable as $key => $entry ) { |
1389 | $entryBatch[$key] = $entry; |
1390 | if ( count( $entryBatch ) >= $batchSize ) { |
1391 | yield $entryBatch; |
1392 | $entryBatch = []; |
1393 | } |
1394 | } |
1395 | if ( $entryBatch ) { |
1396 | yield $entryBatch; |
1397 | } |
1398 | } |
1399 | |
1400 | /** |
1401 | * Count down from $seconds to zero on the terminal, with a one-second pause |
1402 | * between showing each number. If the maintenance script is in quiet mode, |
1403 | * this function does nothing. |
1404 | * |
1405 | * @since 1.31 |
1406 | * |
1407 | * @codeCoverageIgnore |
1408 | * @param int $seconds |
1409 | */ |
1410 | protected function countDown( $seconds ) { |
1411 | if ( $this->isQuiet() ) { |
1412 | return; |
1413 | } |
1414 | for ( $i = $seconds; $i >= 0; $i-- ) { |
1415 | if ( $i != $seconds ) { |
1416 | $this->output( str_repeat( "\x08", strlen( (string)( $i + 1 ) ) ) ); |
1417 | } |
1418 | $this->output( (string)$i ); |
1419 | if ( $i ) { |
1420 | sleep( 1 ); |
1421 | } |
1422 | } |
1423 | $this->output( "\n" ); |
1424 | } |
1425 | |
1426 | /** |
1427 | * Wrapper for posix_isatty() |
1428 | * We default as considering stdin a tty (for nice readline methods) |
1429 | * but treating stout as not a tty to avoid color codes |
1430 | * |
1431 | * @param mixed $fd File descriptor |
1432 | * @return bool |
1433 | */ |
1434 | public static function posix_isatty( $fd ) { |
1435 | if ( !function_exists( 'posix_isatty' ) ) { |
1436 | return !$fd; |
1437 | } |
1438 | |
1439 | return posix_isatty( $fd ); |
1440 | } |
1441 | |
1442 | /** |
1443 | * Prompt the console for input |
1444 | * @param string $prompt What to begin the line with, like '> ' |
1445 | * @return string|false Response |
1446 | */ |
1447 | public static function readconsole( $prompt = '> ' ) { |
1448 | static $isatty = null; |
1449 | $isatty ??= self::posix_isatty( 0 /*STDIN*/ ); |
1450 | |
1451 | if ( $isatty && function_exists( 'readline' ) ) { |
1452 | return readline( $prompt ); |
1453 | } |
1454 | |
1455 | if ( $isatty ) { |
1456 | $st = self::readlineEmulation( $prompt ); |
1457 | } elseif ( feof( STDIN ) ) { |
1458 | $st = false; |
1459 | } else { |
1460 | $st = fgets( STDIN, 1024 ); |
1461 | } |
1462 | if ( $st === false ) { |
1463 | return false; |
1464 | } |
1465 | |
1466 | return trim( $st ); |
1467 | } |
1468 | |
1469 | /** |
1470 | * Emulate readline() |
1471 | * @param string $prompt What to begin the line with, like '> ' |
1472 | * @return string|false |
1473 | */ |
1474 | private static function readlineEmulation( $prompt ) { |
1475 | $bash = ExecutableFinder::findInDefaultPaths( 'bash' ); |
1476 | if ( !wfIsWindows() && $bash ) { |
1477 | $encPrompt = Shell::escape( $prompt ); |
1478 | $command = "read -er -p $encPrompt && echo \"\$REPLY\""; |
1479 | $result = Shell::command( $bash, '-c', $command ) |
1480 | ->passStdin() |
1481 | ->forwardStderr() |
1482 | ->execute(); |
1483 | |
1484 | if ( $result->getExitCode() == 0 ) { |
1485 | return $result->getStdout(); |
1486 | } |
1487 | |
1488 | if ( $result->getExitCode() == 127 ) { |
1489 | // Couldn't execute bash even though we thought we saw it. |
1490 | // Shell probably spit out an error message, sorry :( |
1491 | // Fall through to fgets()... |
1492 | } else { |
1493 | // EOF/ctrl+D |
1494 | return false; |
1495 | } |
1496 | } |
1497 | |
1498 | // Fallback... we'll have no editing controls, EWWW |
1499 | if ( feof( STDIN ) ) { |
1500 | return false; |
1501 | } |
1502 | print $prompt; |
1503 | |
1504 | return fgets( STDIN, 1024 ); |
1505 | } |
1506 | |
1507 | /** |
1508 | * Get the terminal size as a two-element array where the first element |
1509 | * is the width (number of columns) and the second element is the height |
1510 | * (number of rows). |
1511 | * |
1512 | * @return array |
1513 | */ |
1514 | public static function getTermSize() { |
1515 | static $termSize = null; |
1516 | |
1517 | if ( $termSize !== null ) { |
1518 | return $termSize; |
1519 | } |
1520 | |
1521 | $default = [ 80, 50 ]; |
1522 | |
1523 | if ( wfIsWindows() || Shell::isDisabled() ) { |
1524 | $termSize = $default; |
1525 | |
1526 | return $termSize; |
1527 | } |
1528 | |
1529 | // It's possible to get the screen size with VT-100 terminal escapes, |
1530 | // but reading the responses is not possible without setting raw mode |
1531 | // (unless you want to require the user to press enter), and that |
1532 | // requires an ioctl(), which we can't do. So we have to shell out to |
1533 | // something that can do the relevant syscalls. There are a few |
1534 | // options. Linux and Mac OS X both have "stty size" which does the |
1535 | // job directly. |
1536 | $result = Shell::command( 'stty', 'size' )->passStdin()->execute(); |
1537 | if ( $result->getExitCode() !== 0 || |
1538 | !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m ) |
1539 | ) { |
1540 | $termSize = $default; |
1541 | |
1542 | return $termSize; |
1543 | } |
1544 | |
1545 | $termSize = [ intval( $m[2] ), intval( $m[1] ) ]; |
1546 | |
1547 | return $termSize; |
1548 | } |
1549 | |
1550 | /** |
1551 | * Call this to set up the autoloader to allow classes to be used from the |
1552 | * tests directory. |
1553 | * |
1554 | * @deprecated since 1.41. Set the MW_AUTOLOAD_TEST_CLASSES in file scope instead. |
1555 | */ |
1556 | public static function requireTestsAutoloader() { |
1557 | require_once __DIR__ . '/../../tests/common/TestsAutoLoader.php'; |
1558 | } |
1559 | |
1560 | /** |
1561 | * Get a HookContainer, for running extension hooks or for hook metadata. |
1562 | * |
1563 | * @since 1.35 |
1564 | * @return HookContainer |
1565 | */ |
1566 | protected function getHookContainer() { |
1567 | if ( !$this->hookContainer ) { |
1568 | $this->hookContainer = $this->getServiceContainer()->getHookContainer(); |
1569 | } |
1570 | return $this->hookContainer; |
1571 | } |
1572 | |
1573 | /** |
1574 | * Get a HookRunner for running core hooks. |
1575 | * |
1576 | * @internal This is for use by core only. Hook interfaces may be removed |
1577 | * without notice. |
1578 | * @since 1.35 |
1579 | * @return HookRunner |
1580 | */ |
1581 | protected function getHookRunner() { |
1582 | if ( !$this->hookRunner ) { |
1583 | $this->hookRunner = new HookRunner( $this->getHookContainer() ); |
1584 | } |
1585 | return $this->hookRunner; |
1586 | } |
1587 | |
1588 | /** |
1589 | * Utility function to parse a string (perhaps from a command line option) |
1590 | * into a list of integers (perhaps some kind of numeric IDs). |
1591 | * |
1592 | * @since 1.35 |
1593 | * |
1594 | * @param string $text |
1595 | * |
1596 | * @return int[] |
1597 | */ |
1598 | protected function parseIntList( $text ) { |
1599 | $ids = preg_split( '/[\s,;:|]+/', $text ); |
1600 | $ids = array_map( |
1601 | static function ( $id ) { |
1602 | return (int)$id; |
1603 | }, |
1604 | $ids |
1605 | ); |
1606 | return array_filter( $ids ); |
1607 | } |
1608 | |
1609 | /** |
1610 | * @param string $errorMsg Error message to be displayed if neither --user or --userid are passed. |
1611 | * |
1612 | * @since 1.37 |
1613 | * |
1614 | * @return User |
1615 | */ |
1616 | protected function validateUserOption( $errorMsg ) { |
1617 | if ( $this->hasOption( "user" ) ) { |
1618 | $user = User::newFromName( $this->getOption( 'user' ) ); |
1619 | } elseif ( $this->hasOption( "userid" ) ) { |
1620 | $user = User::newFromId( $this->getOption( 'userid' ) ); |
1621 | } else { |
1622 | $this->fatalError( $errorMsg ); |
1623 | } |
1624 | if ( !$user || !$user->isRegistered() ) { |
1625 | if ( $this->hasOption( "user" ) ) { |
1626 | $this->fatalError( "No such user: " . $this->getOption( 'user' ) ); |
1627 | } elseif ( $this->hasOption( "userid" ) ) { |
1628 | $this->fatalError( "No such user id: " . $this->getOption( 'userid' ) ); |
1629 | } |
1630 | } |
1631 | |
1632 | return $user; |
1633 | } |
1634 | |
1635 | /** |
1636 | * @param string $prompt The prompt to display to the user |
1637 | * @param string|null $default The default value to return if the user just presses enter |
1638 | * |
1639 | * @return string|null |
1640 | * |
1641 | * @since 1.43 |
1642 | */ |
1643 | protected function prompt( string $prompt, ?string $default = null ): ?string { |
1644 | $defaultText = $default === null ? ' > ' : " [{$default}] > "; |
1645 | $promptWithDefault = $prompt . $defaultText; |
1646 | $line = self::readconsole( $promptWithDefault ); |
1647 | if ( $line === false ) { |
1648 | return $default; |
1649 | } |
1650 | if ( $line === '' ) { |
1651 | return $default; |
1652 | } |
1653 | |
1654 | return $line; |
1655 | } |
1656 | } |
1657 | |
1658 | /** @deprecated class alias since 1.43 */ |
1659 | class_alias( Maintenance::class, 'Maintenance' ); |