Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
57.07% |
109 / 191 |
|
12.50% |
1 / 8 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
57.07% |
109 / 191 |
|
12.50% |
1 / 8 |
197.20 | |
0.00% |
0 / 1 |
registerExtension | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
onLoadExtensionSchemaUpdates | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
30 | |||
getDataPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createInitialContent | |
77.50% |
31 / 40 |
|
0.00% |
0 / 1 |
7.56 | |||
insertContentObject | |
83.93% |
47 / 56 |
|
0.00% |
0 / 1 |
15.93 | |||
initializeZObjectJoinTable | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
updateSecondaryTables | |
66.67% |
24 / 36 |
|
0.00% |
0 / 1 |
13.70 | |||
ensureZObjectStoreIsPresent | |
6.25% |
1 / 16 |
|
0.00% |
0 / 1 |
5.30 |
1 | <?php |
2 | /** |
3 | * WikiLambda extension hooks |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @copyright 2020â Abstract Wikipedia team; see AUTHORS.txt |
8 | * @license MIT |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\WikiLambda; |
12 | |
13 | use MediaWiki\CommentStore\CommentStoreComment; |
14 | use MediaWiki\Config\ConfigException; |
15 | use MediaWiki\Deferred\DeferredUpdates; |
16 | use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry; |
17 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
18 | use MediaWiki\Installer\DatabaseUpdater; |
19 | use MediaWiki\Logger\LoggerFactory; |
20 | use MediaWiki\MediaWikiServices; |
21 | use MediaWiki\Revision\SlotRecord; |
22 | use MediaWiki\Title\Title; |
23 | use MediaWiki\User\User; |
24 | use RecentChange; |
25 | use RuntimeException; |
26 | use Wikimedia\Services\NoSuchServiceException; |
27 | |
28 | class Hooks implements \MediaWiki\Installer\Hook\LoadExtensionSchemaUpdatesHook { |
29 | |
30 | public static function registerExtension() { |
31 | // We define the content model regardless of if 'repo mode' is enabled |
32 | require_once __DIR__ . '/defines.php'; |
33 | |
34 | // Can't use MediaWikiServices or config objects yet, so use globals |
35 | global $wgWikiLambdaEnableRepoMode; |
36 | if ( !$wgWikiLambdaEnableRepoMode ) { |
37 | // Nothing for us to do. |
38 | return; |
39 | } |
40 | |
41 | global $wgNamespaceContentModels; |
42 | $wgNamespaceContentModels[ NS_MAIN ] = CONTENT_MODEL_ZOBJECT; |
43 | |
44 | global $wgNamespaceProtection; |
45 | $wgNamespaceProtection[ NS_MAIN ] = [ 'wikilambda-edit', 'wikilambda-create' ]; |
46 | |
47 | // (T267232) Prevent ZObject pages from being transcluded; sadly this isn't available as |
48 | // an extension.json attribute as of yet. |
49 | global $wgNonincludableNamespaces; |
50 | $wgNonincludableNamespaces[] = NS_MAIN; |
51 | } |
52 | |
53 | /** |
54 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdates |
55 | * |
56 | * @param DatabaseUpdater $updater DatabaseUpdater subclass |
57 | */ |
58 | public function onLoadExtensionSchemaUpdates( $updater ) { |
59 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
60 | // Special form of the check to call has() first, as we'll likely be in pre-extension registry mode |
61 | if ( !$config->has( 'WikiLambdaEnableRepoMode' ) || !$config->get( 'WikiLambdaEnableRepoMode' ) ) { |
62 | // Nothing for us to do. |
63 | return; |
64 | } |
65 | |
66 | $db = $updater->getDB(); |
67 | $type = $db->getType(); |
68 | $dir = __DIR__ . '/../sql'; |
69 | |
70 | if ( !in_array( $type, [ 'mysql', 'sqlite', 'postgres' ] ) ) { |
71 | wfWarn( "Database type '$type' is not supported by the WikiLambda extension." ); |
72 | return; |
73 | } |
74 | |
75 | $tables = [ |
76 | 'zobject_labels', |
77 | 'zobject_label_conflicts', |
78 | 'zobject_function_join', |
79 | 'ztester_results', |
80 | 'zlanguages', |
81 | 'zobject_join' |
82 | ]; |
83 | |
84 | foreach ( $tables as $key => $table ) { |
85 | $updater->addExtensionTable( 'wikilambda_' . $table, "$dir/$type/table-$table.sql" ); |
86 | } |
87 | |
88 | // Database updates: |
89 | // (T285368) Add primary label field to labels table |
90 | $updater->addExtensionField( |
91 | 'wikilambda_zobject_labels', |
92 | 'wlzl_label_primary', |
93 | "$dir/$type/patch-add-primary-label-field.sql" |
94 | ); |
95 | |
96 | // (T262089) Add return type field to labels table |
97 | $updater->addExtensionField( |
98 | 'wikilambda_zobject_labels', |
99 | 'wlzl_return_type', |
100 | "$dir/$type/patch-add-return-type-field.sql" |
101 | ); |
102 | |
103 | $updater->addExtensionUpdate( [ [ self::class, 'createInitialContent' ] ] ); |
104 | $updater->addExtensionUpdate( [ [ self::class, 'initializeZObjectJoinTable' ] ] ); |
105 | } |
106 | |
107 | /** |
108 | * Return path of data definition JSON files. |
109 | * |
110 | * @return string |
111 | */ |
112 | protected static function getDataPath() { |
113 | return dirname( __DIR__ ) . '/function-schemata/data/definitions/'; |
114 | } |
115 | |
116 | /** |
117 | * Installer/Updater callback to create the initial "system" ZObjects on any installation. This |
118 | * is a callback so that it runs after the tables have been created/updated. |
119 | * |
120 | * @param DatabaseUpdater $updater |
121 | * @param bool $overwrite If true, overwrites the content, else skips if present |
122 | */ |
123 | public static function createInitialContent( DatabaseUpdater $updater, $overwrite = false ) { |
124 | // Ensure that the extension is set up (namespace is defined) even when running in update.php outside of MW. |
125 | self::registerExtension(); |
126 | |
127 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
128 | if ( !$config->get( 'WikiLambdaEnableRepoMode' ) ) { |
129 | // Nothing for us to do. |
130 | return; |
131 | } |
132 | |
133 | $contentHandler = MediaWikiServices::getInstance()->getContentHandlerFactory(); |
134 | if ( !$contentHandler->isDefinedModel( CONTENT_MODEL_ZOBJECT ) ) { |
135 | if ( method_exists( $contentHandler, 'defineContentHandler' ) ) { |
136 | // @phan-suppress-next-line PhanUndeclaredMethod this apparently phan doesn't take the hint above |
137 | $contentHandler->defineContentHandler( CONTENT_MODEL_ZOBJECT, ZObjectContentHandler::class ); |
138 | } else { |
139 | throw new ConfigException( 'WikiLambda content model is not registered and we cannot inject it.' ); |
140 | } |
141 | } |
142 | |
143 | // Note: Hard-coding the English version for messages as this can run without a Context and so no language set. |
144 | $creatingUserName = wfMessage( 'wikilambda-systemuser' )->inLanguage( 'en' )->text(); |
145 | $creatingUser = User::newSystemUser( $creatingUserName, [ 'steal' => true ] ); |
146 | // We use wikilambda-bootstrapupdatingeditsummary in maintenance scripts when updating. |
147 | $creatingComment = wfMessage( 'wikilambda-bootstrapcreationeditsummary' )->inLanguage( 'en' )->text(); |
148 | |
149 | if ( !$creatingUser ) { |
150 | // Something went wrong, give up. |
151 | return; |
152 | } |
153 | |
154 | $initialDataToLoadPath = static::getDataPath(); |
155 | |
156 | $dependenciesFile = file_get_contents( $initialDataToLoadPath . 'dependencies.json' ); |
157 | if ( $dependenciesFile === false ) { |
158 | throw new RuntimeException( |
159 | 'Could not load dependencies file from function-schemata sub-repository of the WikiLambda extension.' |
160 | . ' Have you initiated & fetched it? Try `git submodule update --init --recursive`.' |
161 | ); |
162 | } |
163 | $dependencies = json_decode( $dependenciesFile, true ); |
164 | |
165 | $initialDataToLoadListing = array_filter( |
166 | scandir( $initialDataToLoadPath ), |
167 | static function ( $key ) { |
168 | return (bool)preg_match( '/^Z\d+\.json$/', $key ); |
169 | } |
170 | ); |
171 | |
172 | // Naturally sort, so Z2/Persistent Object gets created before others |
173 | natsort( $initialDataToLoadListing ); |
174 | |
175 | $inserted = []; |
176 | foreach ( $initialDataToLoadListing as $filename ) { |
177 | static::insertContentObject( |
178 | $updater, |
179 | $filename, |
180 | $dependencies, |
181 | $creatingUser, |
182 | $creatingComment, |
183 | $overwrite, |
184 | $inserted |
185 | ); |
186 | } |
187 | } |
188 | |
189 | /** |
190 | * Inserts into the database the ZObject found in a given filename of the data directory. First checks |
191 | * whether the ZObject has any dependencies, according to the dependencies.json manifest file, and if so, |
192 | * inserts all the dependencies before trying the current ZObject. |
193 | * |
194 | * Runs in a static context and so can't be part of the normal code in ZObjectStore. |
195 | * |
196 | * @param DatabaseUpdater $updater |
197 | * @param string $filename |
198 | * @param array $dependencies |
199 | * @param User $user |
200 | * @param string $comment |
201 | * @param bool $overwrite |
202 | * @param string[] &$inserted |
203 | * @param string[] $track |
204 | * @return bool Has successfully inserted the content object |
205 | */ |
206 | protected static function insertContentObject( |
207 | $updater, $filename, $dependencies, $user, $comment, $overwrite = false, &$inserted = [], $track = [] |
208 | ) { |
209 | $initialDataToLoadPath = static::getDataPath(); |
210 | $updateRowName = "create WikiLambda initial content - $filename"; |
211 | |
212 | $langReg = ZLangRegistry::singleton(); |
213 | $typeReg = ZTypeRegistry::singleton(); |
214 | |
215 | // Check dependencies |
216 | $zid = substr( $filename, 0, -5 ); |
217 | |
218 | if ( array_key_exists( $zid, $dependencies ) ) { |
219 | $deps = $dependencies[ $zid ]; |
220 | foreach ( $deps as $dep ) { |
221 | if ( |
222 | // Avoid circular dependencies |
223 | !in_array( $dep, $track ) |
224 | && !$langReg->isZidCached( $dep ) |
225 | && !$typeReg->isZidCached( $dep ) |
226 | ) { |
227 | // Call recursively till all dependencies have been added |
228 | $success = self::insertContentObject( |
229 | $updater, |
230 | "$dep.json", |
231 | $dependencies, |
232 | $user, |
233 | $comment, |
234 | $overwrite, |
235 | $inserted, |
236 | array_merge( $track, (array)$dep ) |
237 | ); |
238 | // If any of the dependencies fail, we desist on the current insertion |
239 | if ( !$success ) { |
240 | return false; |
241 | } |
242 | } |
243 | } |
244 | } |
245 | |
246 | $zid = substr( $filename, 0, -5 ); |
247 | $title = Title::newFromText( $zid, NS_MAIN ); |
248 | $services = MediaWikiServices::getInstance(); |
249 | $page = $services->getWikiPageFactory()->newFromTitle( $title ); |
250 | |
251 | // If we don't want to overwrite the ZObjects, and if Zid has already been inserted, |
252 | // just purge the page to update secondary data and return true |
253 | if ( |
254 | ( $overwrite && in_array( $zid, $inserted ) ) || |
255 | ( !$overwrite && $updater->updateRowExists( $updateRowName ) ) |
256 | ) { |
257 | $page->doPurge(); |
258 | return true; |
259 | } |
260 | |
261 | $data = file_get_contents( $initialDataToLoadPath . $filename ); |
262 | if ( !$data ) { |
263 | // something went wrong, give up. |
264 | $updater->output( "\tâ Unable to load file contents for {$title->getPrefixedText()}.\n" ); |
265 | return false; |
266 | } |
267 | |
268 | try { |
269 | $content = ZObjectContentHandler::makeContent( $data, $title ); |
270 | } catch ( ZErrorException $e ) { |
271 | $updater->output( "\tâ Unable to make a ZObject for {$title->getPrefixedText()}.\n" ); |
272 | return false; |
273 | } |
274 | |
275 | static::ensureZObjectStoreIsPresent( $services ); |
276 | |
277 | $pageUpdater = $page->newPageUpdater( $user ); |
278 | $pageUpdater->setContent( SlotRecord::MAIN, $content ); |
279 | $pageUpdater->setRcPatrolStatus( RecentChange::PRC_PATROLLED ); |
280 | |
281 | $pageUpdater->saveRevision( |
282 | CommentStoreComment::newUnsavedComment( $comment ?? '' ), |
283 | EDIT_AUTOSUMMARY | EDIT_NEW |
284 | ); |
285 | |
286 | if ( $pageUpdater->wasSuccessful() ) { |
287 | array_push( $inserted, $zid ); |
288 | $updater->insertUpdateRow( $updateRowName ); |
289 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
290 | // Don't log this during unit testing, quibble thinks it means we're broken. |
291 | $updater->output( "\tSuccessfully created {$title->getPrefixedText()}.\n" ); |
292 | } |
293 | } else { |
294 | $firstError = $pageUpdater->getStatus()->getErrors()[0]; |
295 | $error = wfMessage( $firstError[ 'message' ], $firstError[ 'params' ] ); |
296 | $updater->output( "\tâ Unable to make a page for {$title->getPrefixedText()}: $error\n" ); |
297 | } |
298 | |
299 | return $pageUpdater->wasSuccessful(); |
300 | } |
301 | |
302 | /** |
303 | * Installer/Updater callback to ensure that wikilambda_zobject_join has been populated for all |
304 | * existing functions (Z8s). This is a callback so that it runs after the tables have been |
305 | * created/updated. This function can be removed when we are confident that all WikiLambda |
306 | * installations have a fully populated wikilambda_zobject_join table. |
307 | * |
308 | * @param DatabaseUpdater $updater |
309 | */ |
310 | public static function initializeZObjectJoinTable( DatabaseUpdater $updater ) { |
311 | $updateKey = 'Initialized wikilambda_zobject_join for Z8s'; |
312 | |
313 | if ( !$updater->updateRowExists( $updateKey ) ) { |
314 | static::updateSecondaryTables( $updater, 'Z8' ); |
315 | $updater->insertUpdateRow( $updateKey ); |
316 | } else { |
317 | $updater->output( "...wikilambda_zobject_join table already initialized\n" ); |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * Ensures that secondary DB tables have been populated for ZObjects of the given zType. For |
323 | * each such ZObject, a new instance of ZObjectSecondaryDataUpdate is created and added to |
324 | * DeferredUpdates. |
325 | * |
326 | * N.B. This function assumes that wikilambda_zobject_labels is fully populated; it calls |
327 | * fetchZidsOfType to get a list of ZObjects of the given zType. |
328 | * |
329 | * Note there is a WikiLambda maintenance script (updateSecondaryTables.php) that provides |
330 | * similar functionality (and with some code that duplicates what's here, which could not |
331 | * easily be avoided). |
332 | * |
333 | * @param DatabaseUpdater $updater |
334 | * @param string $zType The type of ZObject for which to do updates. |
335 | * @param bool $verbose If true, print the ZID of each ZObject for which updating is done |
336 | * (default = false) |
337 | * @param bool $dryRun If true, do nothing, just print the output statements |
338 | * (default = false) |
339 | */ |
340 | public static function updateSecondaryTables( |
341 | $updater, |
342 | $zType, |
343 | $verbose = false, |
344 | $dryRun = false |
345 | ) { |
346 | $services = MediaWikiServices::getInstance(); |
347 | |
348 | $config = $services->getMainConfig(); |
349 | if ( !$config->get( 'WikiLambdaEnableRepoMode' ) ) { |
350 | // Nothing for us to do. |
351 | return; |
352 | } |
353 | |
354 | static::ensureZObjectStoreIsPresent( $services ); |
355 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
356 | $handler = new ZObjectContentHandler( CONTENT_MODEL_ZOBJECT ); |
357 | |
358 | $targets = $zObjectStore->fetchZidsOfType( $zType ); |
359 | |
360 | if ( count( $targets ) === 0 ) { |
361 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
362 | // Don't output during unit testing; causes the test to be labeled as risky. |
363 | $updater->output( "No ZObjects of type " . $zType . " for which secondary tables need updating\n" ); |
364 | } |
365 | return; |
366 | } |
367 | |
368 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
369 | // Don't output during unit testing; causes the test to be labeled as risky. |
370 | if ( $dryRun ) { |
371 | $updater->output( "Would have updated" ); |
372 | } else { |
373 | $updater->output( "Updating" ); |
374 | } |
375 | $updater->output( " secondary tables for " . count( $targets ) . " ZObjects of type " . |
376 | $zType . "\n" ); |
377 | } |
378 | |
379 | $offset = 0; |
380 | $queryLimit = 10; |
381 | do { |
382 | $contents = $zObjectStore->fetchBatchZObjects( array_slice( $targets, $offset, |
383 | $queryLimit ) ); |
384 | $offset += $queryLimit; |
385 | |
386 | foreach ( $contents as $zid => $persistentObject ) { |
387 | if ( $verbose ) { |
388 | $updater->output( " $zid" ); |
389 | } |
390 | if ( $dryRun ) { |
391 | continue; |
392 | } |
393 | $title = Title::newFromText( $zid, NS_MAIN ); |
394 | $data = json_encode( $persistentObject->getSerialized() ); |
395 | $content = $handler::makeContent( $data, $title ); |
396 | $update = new ZObjectSecondaryDataUpdate( $title, $content ); |
397 | DeferredUpdates::addUpdate( $update ); |
398 | } |
399 | if ( $verbose ) { |
400 | $updater->output( "\n" ); |
401 | } |
402 | |
403 | } while ( count( $targets ) - $offset > 0 ); |
404 | } |
405 | |
406 | /** |
407 | * Checks for the existence of WikiLambdaZObjectStore in $services, and creates it |
408 | * if needed. |
409 | * |
410 | * @param MediaWikiServices $services |
411 | */ |
412 | private static function ensureZObjectStoreIsPresent( $services ) { |
413 | // If we're in the installer, it won't have registered our extension's services yet. |
414 | try { |
415 | $services->get( 'WikiLambdaZObjectStore' ); |
416 | } catch ( NoSuchServiceException $e ) { |
417 | |
418 | $zObjectStore = new ZObjectStore( |
419 | $services->getDBLoadBalancerFactory(), |
420 | $services->getTitleFactory(), |
421 | $services->getWikiPageFactory(), |
422 | $services->getRevisionStore(), |
423 | $services->getUserGroupManager(), |
424 | LoggerFactory::getInstance( 'WikiLambda' ) |
425 | ); |
426 | $services->defineService( |
427 | 'WikiLambdaZObjectStore', |
428 | static function () use ( $zObjectStore ) { |
429 | return $zObjectStore; |
430 | } |
431 | ); |
432 | } |
433 | } |
434 | } |