Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 395 |
|
0.00% |
0 / 53 |
CRAP | |
0.00% |
0 / 1 |
DatabaseUpdater | |
0.00% |
0 / 393 |
|
0.00% |
0 / 53 |
20880 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
loadExtensionSchemaUpdates | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadExtensions | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
132 | |||
newForDB | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
setAutoExtensionHookContainer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDB | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
output | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
addExtensionUpdate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionUpdateOnVirtualDomain | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropExtensionField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropExtensionIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropExtensionTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renameExtensionIndex | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
modifyExtensionField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
modifyExtensionTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tableExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fieldExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addPostDatabaseUpdateMaintenance | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtensionUpdates | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPostDatabaseUpdateMaintenance | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
writeSchemaUpdateFile | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getSchemaVars | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doUpdates | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
runUpdates | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
110 | |||
updateRowExists | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
insertUpdateRow | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
doTable | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getCoreUpdateList | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
copyFile | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
appendLine | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
applyPatch | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
patchPath | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
addTable | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
addField | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
addIndex | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
dropField | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
dropIndex | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
renameIndex | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
dropTable | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
modifyField | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
modifyTable | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
runMaintenance | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setFileAccess | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
purgeCache | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
checkStats | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
doCollationUpdate | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
doConvertDjvuMetadata | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
rebuildLocalisationCache | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
migrateTemplatelinks | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
ifTableNotExists | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
ifFieldExists | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | /** |
3 | * DBMS-specific updater helper. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Installer |
22 | */ |
23 | |
24 | namespace MediaWiki\Installer; |
25 | |
26 | use AddRFCandPMIDInterwiki; |
27 | use AutoLoader; |
28 | use CleanupEmptyCategories; |
29 | use DeleteDefaultMessages; |
30 | use ExtensionRegistry; |
31 | use FakeMaintenance; |
32 | use FixDefaultJsonContentPages; |
33 | use LogicException; |
34 | use Maintenance; |
35 | use MediaWiki\HookContainer\HookContainer; |
36 | use MediaWiki\HookContainer\HookRunner; |
37 | use MediaWiki\HookContainer\StaticHookRegistry; |
38 | use MediaWiki\MainConfigNames; |
39 | use MediaWiki\MediaWikiServices; |
40 | use MediaWiki\ResourceLoader\MessageBlobStore; |
41 | use MediaWiki\SiteStats\SiteStatsInit; |
42 | use MigrateLinksTable; |
43 | use PopulateBacklinkNamespace; |
44 | use PopulateFilearchiveSha1; |
45 | use PopulateImageSha1; |
46 | use PopulateIpChanges; |
47 | use PopulatePPSortKey; |
48 | use PopulateRevisionLength; |
49 | use PopulateRevisionSha1; |
50 | use RebuildLocalisationCache; |
51 | use RefreshImageMetadata; |
52 | use RuntimeException; |
53 | use UnexpectedValueException; |
54 | use UpdateCollation; |
55 | use Wikimedia\Rdbms\IDatabase; |
56 | use Wikimedia\Rdbms\IMaintainableDatabase; |
57 | use Wikimedia\Rdbms\Platform\ISQLPlatform; |
58 | |
59 | require_once __DIR__ . '/../../maintenance/Maintenance.php'; |
60 | |
61 | /** |
62 | * Class for handling database updates. |
63 | * |
64 | * @ingroup Installer |
65 | * @since 1.17 |
66 | */ |
67 | abstract class DatabaseUpdater { |
68 | public const REPLICATION_WAIT_TIMEOUT = 300; |
69 | |
70 | /** |
71 | * Array of updates to perform on the database |
72 | * |
73 | * @var array |
74 | */ |
75 | protected $updates = []; |
76 | |
77 | /** |
78 | * Array of updates that were skipped |
79 | * |
80 | * @var array |
81 | */ |
82 | protected $updatesSkipped = []; |
83 | |
84 | /** |
85 | * List of extension-provided database updates |
86 | * @var array |
87 | */ |
88 | protected $extensionUpdates = []; |
89 | |
90 | /** |
91 | * List of extension-provided database updates on virtual domain dbs |
92 | * @var array |
93 | */ |
94 | protected $extensionUpdatesWithVirtualDomains = []; |
95 | |
96 | /** |
97 | * Handle to the database subclass |
98 | * |
99 | * @var IMaintainableDatabase |
100 | */ |
101 | protected $db; |
102 | |
103 | /** |
104 | * @var Maintenance |
105 | */ |
106 | protected $maintenance; |
107 | |
108 | protected $shared = false; |
109 | |
110 | /** @var HookContainer|null */ |
111 | protected $autoExtensionHookContainer; |
112 | |
113 | /** |
114 | * @var string[] Scripts to run after database update |
115 | * Should be a subclass of LoggedUpdateMaintenance |
116 | */ |
117 | protected $postDatabaseUpdateMaintenance = [ |
118 | DeleteDefaultMessages::class, |
119 | PopulateRevisionLength::class, |
120 | PopulateRevisionSha1::class, |
121 | PopulateImageSha1::class, |
122 | PopulateFilearchiveSha1::class, |
123 | PopulateBacklinkNamespace::class, |
124 | FixDefaultJsonContentPages::class, |
125 | CleanupEmptyCategories::class, |
126 | AddRFCandPMIDInterwiki::class, |
127 | PopulatePPSortKey::class, |
128 | PopulateIpChanges::class, |
129 | ]; |
130 | |
131 | /** |
132 | * File handle for SQL output. |
133 | * |
134 | * @var resource|null |
135 | */ |
136 | protected $fileHandle = null; |
137 | |
138 | /** |
139 | * Flag specifying whether to skip schema (e.g., SQL-only) updates. |
140 | * |
141 | * @var bool |
142 | */ |
143 | protected $skipSchema = false; |
144 | |
145 | /** |
146 | * @param IMaintainableDatabase &$db To perform updates on |
147 | * @param bool $shared Whether to perform updates on shared tables |
148 | * @param Maintenance|null $maintenance Maintenance object which created us |
149 | */ |
150 | protected function __construct( |
151 | IMaintainableDatabase &$db, |
152 | $shared, |
153 | Maintenance $maintenance = null |
154 | ) { |
155 | $this->db = $db; |
156 | $this->db->setFlag( DBO_DDLMODE ); |
157 | $this->shared = $shared; |
158 | if ( $maintenance ) { |
159 | $this->maintenance = $maintenance; |
160 | $this->fileHandle = $maintenance->fileHandle; |
161 | } else { |
162 | $this->maintenance = new FakeMaintenance; |
163 | } |
164 | $this->maintenance->setDB( $db ); |
165 | } |
166 | |
167 | /** |
168 | * Cause extensions to register any updates they need to perform. |
169 | */ |
170 | private function loadExtensionSchemaUpdates() { |
171 | $hookContainer = $this->loadExtensions(); |
172 | ( new HookRunner( $hookContainer ) )->onLoadExtensionSchemaUpdates( $this ); |
173 | } |
174 | |
175 | /** |
176 | * Loads LocalSettings.php, if needed, and initialises everything needed for |
177 | * LoadExtensionSchemaUpdates hook. |
178 | * |
179 | * @return HookContainer |
180 | */ |
181 | private function loadExtensions() { |
182 | if ( $this->autoExtensionHookContainer ) { |
183 | // Already injected by installer |
184 | return $this->autoExtensionHookContainer; |
185 | } |
186 | if ( defined( 'MW_EXTENSIONS_LOADED' ) ) { |
187 | throw new LogicException( __METHOD__ . |
188 | ' apparently called from installer but no hook container was injected' ); |
189 | } |
190 | if ( !defined( 'MEDIAWIKI_INSTALL' ) ) { |
191 | // Running under update.php: use the global locator |
192 | return MediaWikiServices::getInstance()->getHookContainer(); |
193 | } |
194 | $vars = Installer::getExistingLocalSettings(); |
195 | |
196 | $registry = ExtensionRegistry::getInstance(); |
197 | $queue = $registry->getQueue(); |
198 | // Don't accidentally load extensions in the future |
199 | $registry->clearQueue(); |
200 | |
201 | // Read extension.json files |
202 | $extInfo = $registry->readFromQueue( $queue ); |
203 | |
204 | // Merge extension attribute hooks with hooks defined by a .php |
205 | // registration file included from LocalSettings.php |
206 | $legacySchemaHooks = $extInfo['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ?? []; |
207 | if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) { |
208 | $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] ); |
209 | } |
210 | |
211 | // Register classes defined by extensions that are loaded by including of a file that |
212 | // updates global variables, rather than having an extension.json manifest. |
213 | if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) { |
214 | AutoLoader::registerClasses( $vars['wgAutoloadClasses'] ); |
215 | } |
216 | |
217 | // Register class definitions from extension.json files |
218 | if ( !isset( $extInfo['autoloaderPaths'] ) |
219 | || !isset( $extInfo['autoloaderClasses'] ) |
220 | || !isset( $extInfo['autoloaderNS'] ) |
221 | ) { |
222 | // NOTE: protect against changes to the structure of $extInfo. |
223 | // It's volatile, and this usage is easy to miss. |
224 | throw new LogicException( 'Missing autoloader keys from extracted extension info' ); |
225 | } |
226 | AutoLoader::loadFiles( $extInfo['autoloaderPaths'] ); |
227 | AutoLoader::registerClasses( $extInfo['autoloaderClasses'] ); |
228 | AutoLoader::registerNamespaces( $extInfo['autoloaderNS'] ); |
229 | |
230 | return new HookContainer( |
231 | new StaticHookRegistry( |
232 | [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ], |
233 | $extInfo['attributes']['Hooks'] ?? [], |
234 | $extInfo['attributes']['DeprecatedHooks'] ?? [] |
235 | ), |
236 | MediaWikiServices::getInstance()->getObjectFactory() |
237 | ); |
238 | } |
239 | |
240 | /** |
241 | * @param IMaintainableDatabase $db |
242 | * @param bool $shared |
243 | * @param Maintenance|null $maintenance |
244 | * @return DatabaseUpdater |
245 | */ |
246 | public static function newForDB( |
247 | IMaintainableDatabase $db, |
248 | $shared = false, |
249 | Maintenance $maintenance = null |
250 | ) { |
251 | $type = $db->getType(); |
252 | if ( in_array( $type, Installer::getDBTypes() ) ) { |
253 | $class = '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Updater'; |
254 | |
255 | return new $class( $db, $shared, $maintenance ); |
256 | } |
257 | |
258 | throw new UnexpectedValueException( __METHOD__ . ' called for unsupported DB type' ); |
259 | } |
260 | |
261 | /** |
262 | * Set the HookContainer to use for loading extension schema updates. |
263 | * |
264 | * @internal For use by DatabaseInstaller |
265 | * @since 1.36 |
266 | * @param HookContainer $hookContainer |
267 | */ |
268 | public function setAutoExtensionHookContainer( HookContainer $hookContainer ) { |
269 | $this->autoExtensionHookContainer = $hookContainer; |
270 | } |
271 | |
272 | /** |
273 | * Get a database connection to run updates |
274 | * |
275 | * @return IMaintainableDatabase |
276 | */ |
277 | public function getDB() { |
278 | return $this->db; |
279 | } |
280 | |
281 | /** |
282 | * Output some text. If we're running via the web, escape the text first. |
283 | * |
284 | * @param string $str Text to output |
285 | * @param-taint $str escapes_html |
286 | */ |
287 | public function output( $str ) { |
288 | if ( $this->maintenance->isQuiet() ) { |
289 | return; |
290 | } |
291 | if ( MW_ENTRY_POINT !== 'cli' ) { |
292 | $str = htmlspecialchars( $str ); |
293 | } |
294 | echo $str; |
295 | flush(); |
296 | } |
297 | |
298 | /** |
299 | * Add a new update coming from an extension. |
300 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
301 | * |
302 | * @since 1.17 |
303 | * |
304 | * @param array $update The update to run. Format is [ $callback, $params... ] |
305 | * $callback is the method to call; either a DatabaseUpdater method name or a callable. |
306 | * Must be serializable (i.e., no anonymous functions allowed). The rest of the parameters |
307 | * (if any) will be passed to the callback. The first parameter passed to the callback |
308 | * is always this object. |
309 | */ |
310 | public function addExtensionUpdate( array $update ) { |
311 | $this->extensionUpdates[] = $update; |
312 | } |
313 | |
314 | /** |
315 | * Add a new update coming from an extension on virtual domain databases. |
316 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
317 | * |
318 | * @since 1.42 |
319 | * |
320 | * @param array $update The update to run. The format is [ $virtualDomain, $callback, $params... ] |
321 | * similarly to addExtensionUpdate() |
322 | */ |
323 | public function addExtensionUpdateOnVirtualDomain( array $update ) { |
324 | $this->extensionUpdatesWithVirtualDomains[] = $update; |
325 | } |
326 | |
327 | /** |
328 | * Convenience wrapper for addExtensionUpdate() when adding a new table (which |
329 | * is the most common usage of updaters in an extension) |
330 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
331 | * |
332 | * @since 1.18 |
333 | * |
334 | * @param string $tableName Name of table to create |
335 | * @param string $sqlPath Full path to the schema file |
336 | */ |
337 | public function addExtensionTable( $tableName, $sqlPath ) { |
338 | $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ]; |
339 | } |
340 | |
341 | /** |
342 | * Add an index to an existing extension table. |
343 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
344 | * |
345 | * @since 1.19 |
346 | * |
347 | * @param string $tableName |
348 | * @param string $indexName |
349 | * @param string $sqlPath |
350 | */ |
351 | public function addExtensionIndex( $tableName, $indexName, $sqlPath ) { |
352 | $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ]; |
353 | } |
354 | |
355 | /** |
356 | * Add a field to an existing extension table. |
357 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
358 | * |
359 | * @since 1.19 |
360 | * |
361 | * @param string $tableName |
362 | * @param string $columnName |
363 | * @param string $sqlPath |
364 | */ |
365 | public function addExtensionField( $tableName, $columnName, $sqlPath ) { |
366 | $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ]; |
367 | } |
368 | |
369 | /** |
370 | * Drop a field from an extension table. |
371 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
372 | * |
373 | * @since 1.20 |
374 | * |
375 | * @param string $tableName |
376 | * @param string $columnName |
377 | * @param string $sqlPath |
378 | */ |
379 | public function dropExtensionField( $tableName, $columnName, $sqlPath ) { |
380 | $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ]; |
381 | } |
382 | |
383 | /** |
384 | * Drop an index from an extension table |
385 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
386 | * |
387 | * @since 1.21 |
388 | * |
389 | * @param string $tableName |
390 | * @param string $indexName |
391 | * @param string $sqlPath The path to the SQL change path |
392 | */ |
393 | public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) { |
394 | $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ]; |
395 | } |
396 | |
397 | /** |
398 | * Drop an extension table. |
399 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
400 | * |
401 | * @since 1.20 |
402 | * |
403 | * @param string $tableName |
404 | * @param string|bool $sqlPath |
405 | */ |
406 | public function dropExtensionTable( $tableName, $sqlPath = false ) { |
407 | $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ]; |
408 | } |
409 | |
410 | /** |
411 | * Rename an index on an extension table |
412 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
413 | * |
414 | * @since 1.21 |
415 | * |
416 | * @param string $tableName |
417 | * @param string $oldIndexName |
418 | * @param string $newIndexName |
419 | * @param string $sqlPath The path to the SQL change file |
420 | * @param bool $skipBothIndexExistWarning Whether to warn if both the old |
421 | * and the new indexes exist. [facultative; by default, false] |
422 | */ |
423 | public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName, |
424 | $sqlPath, $skipBothIndexExistWarning = false |
425 | ) { |
426 | $this->extensionUpdates[] = [ |
427 | 'renameIndex', |
428 | $tableName, |
429 | $oldIndexName, |
430 | $newIndexName, |
431 | $skipBothIndexExistWarning, |
432 | $sqlPath, |
433 | true |
434 | ]; |
435 | } |
436 | |
437 | /** |
438 | * Modify an existing field in an extension table. |
439 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
440 | * |
441 | * @since 1.21 |
442 | * |
443 | * @param string $tableName |
444 | * @param string $fieldName The field to be modified |
445 | * @param string $sqlPath The path to the SQL patch |
446 | */ |
447 | public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) { |
448 | $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ]; |
449 | } |
450 | |
451 | /** |
452 | * Modify an existing extension table. |
453 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
454 | * |
455 | * @since 1.31 |
456 | * |
457 | * @param string $tableName |
458 | * @param string $sqlPath The path to the SQL patch |
459 | */ |
460 | public function modifyExtensionTable( $tableName, $sqlPath ) { |
461 | $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ]; |
462 | } |
463 | |
464 | /** |
465 | * @since 1.20 |
466 | * |
467 | * @param string $tableName |
468 | * @return bool |
469 | */ |
470 | public function tableExists( $tableName ) { |
471 | return ( $this->db->tableExists( $tableName, __METHOD__ ) ); |
472 | } |
473 | |
474 | /** |
475 | * @since 1.40 |
476 | * |
477 | * @param string $tableName |
478 | * @param string $fieldName |
479 | * @return bool |
480 | */ |
481 | public function fieldExists( $tableName, $fieldName ) { |
482 | return ( $this->db->fieldExists( $tableName, $fieldName, __METHOD__ ) ); |
483 | } |
484 | |
485 | /** |
486 | * Add a maintenance script to be run after the database updates are complete. |
487 | * |
488 | * Script should subclass LoggedUpdateMaintenance |
489 | * |
490 | * @since 1.19 |
491 | * |
492 | * @param string $class Name of a Maintenance subclass |
493 | */ |
494 | public function addPostDatabaseUpdateMaintenance( $class ) { |
495 | $this->postDatabaseUpdateMaintenance[] = $class; |
496 | } |
497 | |
498 | /** |
499 | * Get the list of extension-defined updates |
500 | * |
501 | * @return array |
502 | */ |
503 | protected function getExtensionUpdates() { |
504 | return $this->extensionUpdates; |
505 | } |
506 | |
507 | /** |
508 | * @since 1.17 |
509 | * |
510 | * @return string[] |
511 | */ |
512 | public function getPostDatabaseUpdateMaintenance() { |
513 | return $this->postDatabaseUpdateMaintenance; |
514 | } |
515 | |
516 | /** |
517 | * @since 1.21 |
518 | * |
519 | * Writes the schema updates desired to a file for the DB Admin to run. |
520 | */ |
521 | private function writeSchemaUpdateFile() { |
522 | $updates = $this->updatesSkipped; |
523 | $this->updatesSkipped = []; |
524 | |
525 | foreach ( $updates as [ $func, $args, $origParams ] ) { |
526 | // @phan-suppress-next-line PhanUndeclaredInvokeInCallable |
527 | $func( ...$args ); |
528 | flush(); |
529 | $this->updatesSkipped[] = $origParams; |
530 | } |
531 | } |
532 | |
533 | /** |
534 | * Get appropriate schema variables in the current database connection. |
535 | * |
536 | * This should be called after any request data has been imported, but before |
537 | * any write operations to the database. The result should be passed to the DB |
538 | * setSchemaVars() method. |
539 | * |
540 | * @return array |
541 | * @since 1.28 |
542 | */ |
543 | public function getSchemaVars() { |
544 | return []; // DB-type specific |
545 | } |
546 | |
547 | /** |
548 | * Do all the updates |
549 | * |
550 | * @param array $what What updates to perform |
551 | */ |
552 | public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) { |
553 | $this->db->setSchemaVars( $this->getSchemaVars() ); |
554 | |
555 | $what = array_fill_keys( $what, true ); |
556 | $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null; |
557 | if ( isset( $what['core'] ) ) { |
558 | $this->doCollationUpdate(); |
559 | $this->runUpdates( $this->getCoreUpdateList(), false ); |
560 | } |
561 | if ( isset( $what['extensions'] ) ) { |
562 | $this->loadExtensionSchemaUpdates(); |
563 | $this->runUpdates( $this->getExtensionUpdates(), true ); |
564 | $this->runUpdates( $this->extensionUpdatesWithVirtualDomains, true, true ); |
565 | } |
566 | |
567 | if ( isset( $what['stats'] ) ) { |
568 | $this->checkStats(); |
569 | } |
570 | |
571 | if ( $this->fileHandle ) { |
572 | $this->skipSchema = false; |
573 | $this->writeSchemaUpdateFile(); |
574 | } |
575 | } |
576 | |
577 | /** |
578 | * Helper function for doUpdates() |
579 | * |
580 | * @param array $updates Array of updates to run |
581 | * @param bool $passSelf Whether to pass this object when calling external functions |
582 | * @param bool $hasVirtualDomain Whether the updates' array include virtual domains |
583 | */ |
584 | private function runUpdates( array $updates, $passSelf, $hasVirtualDomain = false ) { |
585 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
586 | $updatesDone = []; |
587 | $updatesSkipped = []; |
588 | foreach ( $updates as $params ) { |
589 | $origParams = $params; |
590 | $oldDb = null; |
591 | $virtualDomain = null; |
592 | if ( $hasVirtualDomain === true ) { |
593 | $virtualDomain = array_shift( $params ); |
594 | $oldDb = $this->db; |
595 | $this->db = $lbFactory->getPrimaryDatabase( $virtualDomain ); |
596 | '@phan-var IMaintainableDatabase $this->db'; |
597 | } |
598 | $func = array_shift( $params ); |
599 | if ( !is_array( $func ) && method_exists( $this, $func ) ) { |
600 | $func = [ $this, $func ]; |
601 | } elseif ( $passSelf ) { |
602 | array_unshift( $params, $this ); |
603 | } |
604 | $ret = $func( ...$params ); |
605 | if ( $hasVirtualDomain === true && $oldDb ) { |
606 | $this->db = $oldDb; |
607 | } |
608 | |
609 | flush(); |
610 | if ( $ret !== false ) { |
611 | $updatesDone[] = $origParams; |
612 | $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] ); |
613 | } else { |
614 | if ( $hasVirtualDomain === true ) { |
615 | $params = $origParams; |
616 | $func = array_shift( $params ); |
617 | } |
618 | $updatesSkipped[] = [ $func, $params, $origParams ]; |
619 | } |
620 | } |
621 | $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped ); |
622 | $this->updates = array_merge( $this->updates, $updatesDone ); |
623 | } |
624 | |
625 | /** |
626 | * Helper function: check if the given key is present in the updatelog table. |
627 | * |
628 | * @param string $key Name of the key to check for |
629 | * @return bool |
630 | */ |
631 | public function updateRowExists( $key ) { |
632 | $row = $this->db->newSelectQueryBuilder() |
633 | ->select( '1 AS X' ) // T67813 |
634 | ->from( 'updatelog' ) |
635 | ->where( [ 'ul_key' => $key ] ) |
636 | ->caller( __METHOD__ )->fetchRow(); |
637 | |
638 | return (bool)$row; |
639 | } |
640 | |
641 | /** |
642 | * Helper function: Add a key to the updatelog table |
643 | * |
644 | * @note Extensions must only use this from within callbacks registered with |
645 | * addExtensionUpdate(). In particular, this method must not be called directly |
646 | * from a LoadExtensionSchemaUpdates handler. |
647 | * |
648 | * @param string $key Name of the key to insert |
649 | * @param string|null $val [optional] Value to insert along with the key |
650 | */ |
651 | public function insertUpdateRow( $key, $val = null ) { |
652 | $this->db->clearFlag( DBO_DDLMODE ); |
653 | $values = [ 'ul_key' => $key ]; |
654 | if ( $val ) { |
655 | $values['ul_value'] = $val; |
656 | } |
657 | $this->db->newInsertQueryBuilder() |
658 | ->insertInto( 'updatelog' ) |
659 | ->ignore() |
660 | ->row( $values ) |
661 | ->caller( __METHOD__ )->execute(); |
662 | $this->db->setFlag( DBO_DDLMODE ); |
663 | } |
664 | |
665 | /** |
666 | * Returns whether updates should be executed on the database table $name. |
667 | * Updates will be prevented if the table is a shared table, and it is not |
668 | * specified to run updates on shared tables. |
669 | * |
670 | * @param string $name Table name |
671 | * @return bool |
672 | */ |
673 | protected function doTable( $name ) { |
674 | global $wgSharedDB, $wgSharedTables; |
675 | |
676 | // Don't bother to check $wgSharedTables if there isn't a shared database |
677 | // or the user actually also wants to do updates on the shared database. |
678 | if ( $wgSharedDB === null || $this->shared ) { |
679 | return true; |
680 | } |
681 | |
682 | if ( in_array( $name, $wgSharedTables ) ) { |
683 | $this->output( "...skipping update to shared table $name.\n" ); |
684 | return false; |
685 | } |
686 | |
687 | return true; |
688 | } |
689 | |
690 | /** |
691 | * Get an array of updates to perform on the database. Should return a |
692 | * multidimensional array. The main key is the MediaWiki version (1.12, |
693 | * 1.13...) with the values being arrays of updates. |
694 | * |
695 | * @return array[] |
696 | */ |
697 | abstract protected function getCoreUpdateList(); |
698 | |
699 | /** |
700 | * Append an SQL fragment to the open file handle. |
701 | * |
702 | * @note protected since 1.35 |
703 | * |
704 | * @param string $filename File name to open |
705 | */ |
706 | protected function copyFile( $filename ) { |
707 | $this->db->sourceFile( |
708 | $filename, |
709 | null, |
710 | null, |
711 | __METHOD__, |
712 | function ( $line ) { |
713 | return $this->appendLine( $line ); |
714 | } |
715 | ); |
716 | } |
717 | |
718 | /** |
719 | * Append a line to the open file handle. The line is assumed to |
720 | * be a complete SQL statement. |
721 | * |
722 | * This is used as a callback for sourceLine(). |
723 | * |
724 | * @note protected since 1.35 |
725 | * |
726 | * @param string $line Text to append to the file |
727 | * @return bool False to skip actually executing the file |
728 | */ |
729 | protected function appendLine( $line ) { |
730 | $line = rtrim( $line ) . ";\n"; |
731 | if ( fwrite( $this->fileHandle, $line ) === false ) { |
732 | throw new RuntimeException( "trouble writing file" ); |
733 | } |
734 | |
735 | return false; |
736 | } |
737 | |
738 | /** |
739 | * Applies a SQL patch |
740 | * |
741 | * @note Do not use this in a LoadExtensionSchemaUpdates handler, |
742 | * use addExtensionUpdate instead! |
743 | * |
744 | * @param string $path Path to the patch file |
745 | * @param bool $isFullPath Whether to treat $path as a relative or not |
746 | * @param string|null $msg Description of the patch |
747 | * @return bool False if the patch was skipped. |
748 | */ |
749 | protected function applyPatch( $path, $isFullPath = false, $msg = null ) { |
750 | $msg ??= "Applying $path patch"; |
751 | if ( $this->skipSchema ) { |
752 | $this->output( "...skipping schema change ($msg).\n" ); |
753 | |
754 | return false; |
755 | } |
756 | |
757 | $this->output( "{$msg}..." ); |
758 | |
759 | if ( !$isFullPath ) { |
760 | $path = $this->patchPath( $this->db, $path ); |
761 | } |
762 | if ( $this->fileHandle !== null ) { |
763 | $this->copyFile( $path ); |
764 | } else { |
765 | $this->db->sourceFile( $path ); |
766 | } |
767 | $this->output( "done.\n" ); |
768 | |
769 | return true; |
770 | } |
771 | |
772 | /** |
773 | * Get the full path of a patch file. Keep in mind this always returns a patch, as |
774 | * it fails back to MySQL if no DB-specific patch can be found |
775 | * |
776 | * @param IDatabase $db |
777 | * @param string $patch The name of the patch, like patch-something.sql |
778 | * @return string Full path to patch file |
779 | */ |
780 | public function patchPath( IDatabase $db, $patch ) { |
781 | $baseDir = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::BaseDirectory ); |
782 | |
783 | $dbType = $db->getType(); |
784 | if ( file_exists( "$baseDir/maintenance/$dbType/archives/$patch" ) ) { |
785 | return "$baseDir/maintenance/$dbType/archives/$patch"; |
786 | } |
787 | |
788 | return "$baseDir/maintenance/archives/$patch"; |
789 | } |
790 | |
791 | /** |
792 | * Add a new table to the database |
793 | * |
794 | * @note Code in a LoadExtensionSchemaUpdates handler should |
795 | * use addExtensionTable instead! |
796 | * |
797 | * @param string $name Name of the new table |
798 | * @param string $patch Path to the patch file |
799 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
800 | * @return bool False if this was skipped because schema changes are skipped |
801 | */ |
802 | protected function addTable( $name, $patch, $fullpath = false ) { |
803 | if ( !$this->doTable( $name ) ) { |
804 | return true; |
805 | } |
806 | |
807 | if ( $this->db->tableExists( $name, __METHOD__ ) ) { |
808 | $this->output( "...$name table already exists.\n" ); |
809 | return true; |
810 | } |
811 | |
812 | return $this->applyPatch( $patch, $fullpath, "Creating $name table" ); |
813 | } |
814 | |
815 | /** |
816 | * Add a new field to an existing table |
817 | * |
818 | * @note Code in a LoadExtensionSchemaUpdates handler should |
819 | * use addExtensionField instead! |
820 | * |
821 | * @param string $table Name of the table to modify |
822 | * @param string $field Name of the new field |
823 | * @param string $patch Path to the patch file |
824 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
825 | * @return bool False if this was skipped because schema changes are skipped |
826 | */ |
827 | protected function addField( $table, $field, $patch, $fullpath = false ) { |
828 | if ( !$this->doTable( $table ) ) { |
829 | return true; |
830 | } |
831 | |
832 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
833 | $this->output( "...$table table does not exist, skipping new field patch.\n" ); |
834 | } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { |
835 | $this->output( "...have $field field in $table table.\n" ); |
836 | } else { |
837 | return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" ); |
838 | } |
839 | |
840 | return true; |
841 | } |
842 | |
843 | /** |
844 | * Add a new index to an existing table |
845 | * |
846 | * @note Code in a LoadExtensionSchemaUpdates handler should |
847 | * use addExtensionIndex instead! |
848 | * |
849 | * @param string $table Name of the table to modify |
850 | * @param string $index Name of the new index |
851 | * @param string $patch Path to the patch file |
852 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
853 | * @return bool False if this was skipped because schema changes are skipped |
854 | */ |
855 | protected function addIndex( $table, $index, $patch, $fullpath = false ) { |
856 | if ( !$this->doTable( $table ) ) { |
857 | return true; |
858 | } |
859 | |
860 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
861 | $this->output( "...skipping: '$table' table doesn't exist yet.\n" ); |
862 | } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { |
863 | $this->output( "...index $index already set on $table table.\n" ); |
864 | } else { |
865 | return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" ); |
866 | } |
867 | |
868 | return true; |
869 | } |
870 | |
871 | /** |
872 | * Drop a field from an existing table |
873 | * |
874 | * @note Code in a LoadExtensionSchemaUpdates handler should |
875 | * use dropExtensionField instead! |
876 | * |
877 | * @param string $table Name of the table to modify |
878 | * @param string $field Name of the old field |
879 | * @param string $patch Path to the patch file |
880 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
881 | * @return bool False if this was skipped because schema changes are skipped |
882 | */ |
883 | protected function dropField( $table, $field, $patch, $fullpath = false ) { |
884 | if ( !$this->doTable( $table ) ) { |
885 | return true; |
886 | } |
887 | |
888 | if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { |
889 | return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" ); |
890 | } |
891 | |
892 | $this->output( "...$table table does not contain $field field.\n" ); |
893 | return true; |
894 | } |
895 | |
896 | /** |
897 | * Drop an index from an existing table |
898 | * |
899 | * @note Code in a LoadExtensionSchemaUpdates handler should |
900 | * use dropExtensionIndex instead! |
901 | * |
902 | * @param string $table Name of the table to modify |
903 | * @param string $index Name of the index |
904 | * @param string $patch Path to the patch file |
905 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
906 | * @return bool False if this was skipped because schema changes are skipped |
907 | */ |
908 | protected function dropIndex( $table, $index, $patch, $fullpath = false ) { |
909 | if ( !$this->doTable( $table ) ) { |
910 | return true; |
911 | } |
912 | |
913 | if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { |
914 | return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" ); |
915 | } |
916 | |
917 | $this->output( "...$index key doesn't exist.\n" ); |
918 | return true; |
919 | } |
920 | |
921 | /** |
922 | * Rename an index from an existing table |
923 | * |
924 | * @note Code in a LoadExtensionSchemaUpdates handler should |
925 | * use renameExtensionIndex instead! |
926 | * |
927 | * @param string $table Name of the table to modify |
928 | * @param string $oldIndex Old name of the index |
929 | * @param string $newIndex New name of the index |
930 | * @param bool $skipBothIndexExistWarning Whether to warn if both the old and new indexes exist. |
931 | * @param string $patch Path to the patch file |
932 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
933 | * @return bool False if this was skipped because schema changes are skipped |
934 | */ |
935 | protected function renameIndex( $table, $oldIndex, $newIndex, |
936 | $skipBothIndexExistWarning, $patch, $fullpath = false |
937 | ) { |
938 | if ( !$this->doTable( $table ) ) { |
939 | return true; |
940 | } |
941 | |
942 | // First requirement: the table must exist |
943 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
944 | $this->output( "...skipping: '$table' table doesn't exist yet.\n" ); |
945 | |
946 | return true; |
947 | } |
948 | |
949 | // Second requirement: the new index must be missing |
950 | if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) { |
951 | $this->output( "...index $newIndex already set on $table table.\n" ); |
952 | if ( !$skipBothIndexExistWarning && |
953 | $this->db->indexExists( $table, $oldIndex, __METHOD__ ) |
954 | ) { |
955 | $this->output( "...WARNING: $oldIndex still exists, despite it has " . |
956 | "been renamed into $newIndex (which also exists).\n" . |
957 | " $oldIndex should be manually removed if not needed anymore.\n" ); |
958 | } |
959 | |
960 | return true; |
961 | } |
962 | |
963 | // Third requirement: the old index must exist |
964 | if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) { |
965 | $this->output( "...skipping: index $oldIndex doesn't exist.\n" ); |
966 | |
967 | return true; |
968 | } |
969 | |
970 | // Requirements have been satisfied, the patch can be applied |
971 | return $this->applyPatch( |
972 | $patch, |
973 | $fullpath, |
974 | "Renaming index $oldIndex into $newIndex to table $table" |
975 | ); |
976 | } |
977 | |
978 | /** |
979 | * If the specified table exists, drop it, or execute the |
980 | * patch if one is provided. |
981 | * |
982 | * @note Code in a LoadExtensionSchemaUpdates handler should |
983 | * use dropExtensionTable instead! |
984 | * |
985 | * @note protected since 1.35 |
986 | * |
987 | * @param string $table Table to drop. |
988 | * @param string|false $patch String of patch file that will drop the table. Default: false. |
989 | * @param bool $fullpath Whether $patch is a full path. Default: false. |
990 | * @return bool False if this was skipped because schema changes are skipped |
991 | */ |
992 | protected function dropTable( $table, $patch = false, $fullpath = false ) { |
993 | if ( !$this->doTable( $table ) ) { |
994 | return true; |
995 | } |
996 | |
997 | if ( $this->db->tableExists( $table, __METHOD__ ) ) { |
998 | $msg = "Dropping table $table"; |
999 | |
1000 | if ( $patch === false ) { |
1001 | $this->output( "$msg ..." ); |
1002 | $this->db->dropTable( $table, __METHOD__ ); |
1003 | $this->output( "done.\n" ); |
1004 | } else { |
1005 | return $this->applyPatch( $patch, $fullpath, $msg ); |
1006 | } |
1007 | } else { |
1008 | $this->output( "...$table doesn't exist.\n" ); |
1009 | } |
1010 | |
1011 | return true; |
1012 | } |
1013 | |
1014 | /** |
1015 | * Modify an existing field |
1016 | * |
1017 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1018 | * use modifyExtensionField instead! |
1019 | * |
1020 | * @note protected since 1.35 |
1021 | * |
1022 | * @param string $table Name of the table to which the field belongs |
1023 | * @param string $field Name of the field to modify |
1024 | * @param string $patch Path to the patch file |
1025 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
1026 | * @return bool False if this was skipped because schema changes are skipped |
1027 | */ |
1028 | protected function modifyField( $table, $field, $patch, $fullpath = false ) { |
1029 | if ( !$this->doTable( $table ) ) { |
1030 | return true; |
1031 | } |
1032 | |
1033 | $updateKey = "$table-$field-$patch"; |
1034 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
1035 | $this->output( "...$table table does not exist, skipping modify field patch.\n" ); |
1036 | } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) { |
1037 | $this->output( "...$field field does not exist in $table table, " . |
1038 | "skipping modify field patch.\n" ); |
1039 | } elseif ( $this->updateRowExists( $updateKey ) ) { |
1040 | $this->output( "...$field in table $table already modified by patch $patch.\n" ); |
1041 | } else { |
1042 | $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" ); |
1043 | if ( $apply ) { |
1044 | $this->insertUpdateRow( $updateKey ); |
1045 | } |
1046 | return $apply; |
1047 | } |
1048 | return true; |
1049 | } |
1050 | |
1051 | /** |
1052 | * Modify an existing table, similar to modifyField. Intended for changes that |
1053 | * touch more than one column on a table. |
1054 | * |
1055 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1056 | * use modifyExtensionTable instead! |
1057 | * |
1058 | * @note protected since 1.35 |
1059 | * |
1060 | * @param string $table Name of the table to modify |
1061 | * @param string $patch Name of the patch file to apply |
1062 | * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false |
1063 | * @return bool False if this was skipped because of schema changes being skipped |
1064 | */ |
1065 | protected function modifyTable( $table, $patch, $fullpath = false ) { |
1066 | if ( !$this->doTable( $table ) ) { |
1067 | return true; |
1068 | } |
1069 | |
1070 | $updateKey = "$table-$patch"; |
1071 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
1072 | $this->output( "...$table table does not exist, skipping modify table patch.\n" ); |
1073 | } elseif ( $this->updateRowExists( $updateKey ) ) { |
1074 | $this->output( "...table $table already modified by patch $patch.\n" ); |
1075 | } else { |
1076 | $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" ); |
1077 | if ( $apply ) { |
1078 | $this->insertUpdateRow( $updateKey ); |
1079 | } |
1080 | return $apply; |
1081 | } |
1082 | return true; |
1083 | } |
1084 | |
1085 | /** |
1086 | * Run a maintenance script |
1087 | * |
1088 | * This should only be used when the maintenance script must run before |
1089 | * later updates. If later updates don't depend on the script, add it to |
1090 | * DatabaseUpdater::$postDatabaseUpdateMaintenance instead. |
1091 | * |
1092 | * The script's execute() method must return true to indicate successful |
1093 | * completion, and must return false (or throw an exception) to indicate |
1094 | * unsuccessful completion. |
1095 | * |
1096 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1097 | * use addExtensionUpdate instead! |
1098 | * |
1099 | * @note protected since 1.35 |
1100 | * |
1101 | * @since 1.32 |
1102 | * @param string $class Maintenance subclass |
1103 | * @param string $script Script path and filename, usually "maintenance/fooBar.php" |
1104 | */ |
1105 | protected function runMaintenance( $class, $script ) { |
1106 | $this->output( "Running $script...\n" ); |
1107 | $task = $this->maintenance->runChild( $class ); |
1108 | $ok = $task->execute(); |
1109 | if ( !$ok ) { |
1110 | throw new RuntimeException( "Execution of $script did not complete successfully." ); |
1111 | } |
1112 | $this->output( "done.\n" ); |
1113 | } |
1114 | |
1115 | /** |
1116 | * Set any .htaccess files or equivalent for storage repos |
1117 | * |
1118 | * Some zones (e.g. "temp") used to be public and may have been initialized as such |
1119 | */ |
1120 | public function setFileAccess() { |
1121 | $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); |
1122 | $zonePath = $repo->getZonePath( 'temp' ); |
1123 | if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) { |
1124 | // If the directory was never made, then it will have the right ACLs when it is made |
1125 | $status = $repo->getBackend()->secure( [ |
1126 | 'dir' => $zonePath, |
1127 | 'noAccess' => true, |
1128 | 'noListing' => true |
1129 | ] ); |
1130 | if ( $status->isOK() ) { |
1131 | $this->output( "Set the local repo temp zone container to be private.\n" ); |
1132 | } else { |
1133 | $this->output( "Failed to set the local repo temp zone container to be private.\n" ); |
1134 | } |
1135 | } |
1136 | } |
1137 | |
1138 | /** |
1139 | * Purge various database caches |
1140 | */ |
1141 | public function purgeCache() { |
1142 | global $wgLocalisationCacheConf; |
1143 | // We can't guarantee that the user will be able to use TRUNCATE, |
1144 | // but we know that DELETE is available to us |
1145 | $this->output( "Purging caches..." ); |
1146 | |
1147 | // ObjectCache |
1148 | $this->db->newDeleteQueryBuilder() |
1149 | ->deleteFrom( 'objectcache' ) |
1150 | ->where( ISQLPlatform::ALL_ROWS ) |
1151 | ->caller( __METHOD__ ) |
1152 | ->execute(); |
1153 | |
1154 | // LocalisationCache |
1155 | if ( $wgLocalisationCacheConf['manualRecache'] ) { |
1156 | $this->rebuildLocalisationCache(); |
1157 | } |
1158 | |
1159 | // ResourceLoader: Message cache |
1160 | $services = MediaWikiServices::getInstance(); |
1161 | MessageBlobStore::clearGlobalCacheEntry( |
1162 | $services->getMainWANObjectCache() |
1163 | ); |
1164 | |
1165 | // ResourceLoader: File-dependency cache |
1166 | $this->db->newDeleteQueryBuilder() |
1167 | ->deleteFrom( 'module_deps' ) |
1168 | ->where( ISQLPlatform::ALL_ROWS ) |
1169 | ->caller( __METHOD__ ) |
1170 | ->execute(); |
1171 | $this->output( "done.\n" ); |
1172 | } |
1173 | |
1174 | /** |
1175 | * Check the site_stats table is not properly populated. |
1176 | */ |
1177 | protected function checkStats() { |
1178 | $this->output( "...site_stats is populated..." ); |
1179 | $row = $this->db->newSelectQueryBuilder() |
1180 | ->select( '*' ) |
1181 | ->from( 'site_stats' ) |
1182 | ->where( [ 'ss_row_id' => 1 ] ) |
1183 | ->caller( __METHOD__ )->fetchRow(); |
1184 | if ( $row === false ) { |
1185 | $this->output( "data is missing! rebuilding...\n" ); |
1186 | } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) { |
1187 | $this->output( "missing ss_total_pages, rebuilding...\n" ); |
1188 | } else { |
1189 | $this->output( "done.\n" ); |
1190 | |
1191 | return; |
1192 | } |
1193 | SiteStatsInit::doAllAndCommit( $this->db ); |
1194 | } |
1195 | |
1196 | # Common updater functions |
1197 | |
1198 | /** |
1199 | * Update CategoryLinks collation |
1200 | */ |
1201 | protected function doCollationUpdate() { |
1202 | global $wgCategoryCollation; |
1203 | if ( $this->updateRowExists( 'UpdateCollation::' . $wgCategoryCollation ) ) { |
1204 | $this->output( "...collations up-to-date.\n" ); |
1205 | return; |
1206 | } |
1207 | $this->output( "Updating category collations...\n" ); |
1208 | $task = $this->maintenance->runChild( UpdateCollation::class ); |
1209 | $ok = $task->execute(); |
1210 | if ( $ok !== false ) { |
1211 | $this->output( "...done.\n" ); |
1212 | $this->insertUpdateRow( 'UpdateCollation::' . $wgCategoryCollation ); |
1213 | } |
1214 | } |
1215 | |
1216 | protected function doConvertDjvuMetadata() { |
1217 | if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) { |
1218 | return; |
1219 | } |
1220 | $this->output( "Converting djvu metadata..." ); |
1221 | $task = $this->maintenance->runChild( RefreshImageMetadata::class ); |
1222 | '@phan-var RefreshImageMetadata $task'; |
1223 | $task->loadParamsAndArgs( RefreshImageMetadata::class, [ |
1224 | 'force' => true, |
1225 | 'mediatype' => 'OFFICE', |
1226 | 'mime' => 'image/*', |
1227 | 'batch-size' => 1, |
1228 | 'sleep' => 1 |
1229 | ] ); |
1230 | $ok = $task->execute(); |
1231 | if ( $ok !== false ) { |
1232 | $this->output( "...done.\n" ); |
1233 | $this->insertUpdateRow( 'ConvertDjvuMetadata' ); |
1234 | } |
1235 | } |
1236 | |
1237 | /** |
1238 | * Rebuilds the localisation cache |
1239 | */ |
1240 | protected function rebuildLocalisationCache() { |
1241 | /** |
1242 | * @var RebuildLocalisationCache $cl |
1243 | */ |
1244 | $cl = $this->maintenance->runChild( |
1245 | RebuildLocalisationCache::class, 'rebuildLocalisationCache.php' |
1246 | ); |
1247 | '@phan-var RebuildLocalisationCache $cl'; |
1248 | $this->output( "Rebuilding localisation cache...\n" ); |
1249 | $cl->setForce(); |
1250 | $cl->execute(); |
1251 | $this->output( "done.\n" ); |
1252 | } |
1253 | |
1254 | protected function migrateTemplatelinks() { |
1255 | if ( $this->updateRowExists( MigrateLinksTable::class . 'templatelinks' ) ) { |
1256 | $this->output( "...templatelinks table has already been migrated.\n" ); |
1257 | return; |
1258 | } |
1259 | /** |
1260 | * @var MigrateLinksTable $task |
1261 | */ |
1262 | $task = $this->maintenance->runChild( |
1263 | MigrateLinksTable::class, 'migrateLinksTable.php' |
1264 | ); |
1265 | '@phan-var MigrateLinksTable $task'; |
1266 | $task->loadParamsAndArgs( MigrateLinksTable::class, [ |
1267 | 'force' => true, |
1268 | 'table' => 'templatelinks' |
1269 | ] ); |
1270 | $this->output( "Running migrateLinksTable.php on templatelinks...\n" ); |
1271 | $task->execute(); |
1272 | $this->output( "done.\n" ); |
1273 | } |
1274 | |
1275 | /** |
1276 | * Only run a function if a table does not exist |
1277 | * |
1278 | * @since 1.35 |
1279 | * @param string $table Table to check. |
1280 | * If passed $this, it's assumed to be a call from runUpdates() with |
1281 | * $passSelf = true: all other parameters are shifted and $this is |
1282 | * prepended to the rest of $params. |
1283 | * @param string|array|static $func Normally this is the string naming the method on $this to |
1284 | * call. It may also be an array style callable. |
1285 | * @param mixed ...$params Parameters for `$func` |
1286 | * @return mixed Whatever $func returns, or null when skipped. |
1287 | */ |
1288 | protected function ifTableNotExists( $table, $func, ...$params ) { |
1289 | // Handle $passSelf from runUpdates(). |
1290 | $passSelf = false; |
1291 | if ( $table === $this ) { |
1292 | $passSelf = true; |
1293 | $table = $func; |
1294 | $func = array_shift( $params ); |
1295 | } |
1296 | |
1297 | if ( $this->db->tableExists( $table, __METHOD__ ) ) { |
1298 | return null; |
1299 | } |
1300 | |
1301 | if ( !is_array( $func ) && method_exists( $this, $func ) ) { |
1302 | $func = [ $this, $func ]; |
1303 | } elseif ( $passSelf ) { |
1304 | array_unshift( $params, $this ); |
1305 | } |
1306 | |
1307 | // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused |
1308 | return $func( ...$params ); |
1309 | } |
1310 | |
1311 | /** |
1312 | * Only run a function if the named field exists |
1313 | * |
1314 | * @since 1.35 |
1315 | * @param string $table Table to check. |
1316 | * If passed $this, it's assumed to be a call from runUpdates() with |
1317 | * $passSelf = true: all other parameters are shifted and $this is |
1318 | * prepended to the rest of $params. |
1319 | * @param string $field Field to check |
1320 | * @param string|array|static $func Normally this is the string naming the method on $this to |
1321 | * call. It may also be an array style callable. |
1322 | * @param mixed ...$params Parameters for `$func` |
1323 | * @return mixed Whatever $func returns, or null when skipped. |
1324 | */ |
1325 | protected function ifFieldExists( $table, $field, $func, ...$params ) { |
1326 | // Handle $passSelf from runUpdates(). |
1327 | $passSelf = false; |
1328 | if ( $table === $this ) { |
1329 | $passSelf = true; |
1330 | $table = $field; |
1331 | $field = $func; |
1332 | $func = array_shift( $params ); |
1333 | } |
1334 | |
1335 | if ( !$this->db->tableExists( $table, __METHOD__ ) || |
1336 | !$this->db->fieldExists( $table, $field, __METHOD__ ) |
1337 | ) { |
1338 | return null; |
1339 | } |
1340 | |
1341 | if ( !is_array( $func ) && method_exists( $this, $func ) ) { |
1342 | $func = [ $this, $func ]; |
1343 | } elseif ( $passSelf ) { |
1344 | array_unshift( $params, $this ); |
1345 | } |
1346 | |
1347 | // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused |
1348 | return $func( ...$params ); |
1349 | } |
1350 | |
1351 | } |
1352 | |
1353 | /** @deprecated class alias since 1.41 */ |
1354 | class_alias( DatabaseUpdater::class, 'DatabaseUpdater' ); |