Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.27% |
81 / 139 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
58.27% |
81 / 139 |
|
40.00% |
2 / 5 |
79.96 | |
0.00% |
0 / 1 |
registerExtension | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
onLoadExtensionSchemaUpdates | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 | |||
getDataPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createInitialContent | |
78.38% |
29 / 37 |
|
0.00% |
0 / 1 |
6.36 | |||
insertContentObject | |
66.20% |
47 / 71 |
|
0.00% |
0 / 1 |
25.89 |
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\Extension\WikiLambda\Registry\ZLangRegistry; |
16 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
17 | use MediaWiki\Installer\DatabaseUpdater; |
18 | use MediaWiki\Logger\LoggerFactory; |
19 | use MediaWiki\MediaWikiServices; |
20 | use MediaWiki\Revision\SlotRecord; |
21 | use MediaWiki\Title\Title; |
22 | use MediaWiki\User\User; |
23 | use RecentChange; |
24 | use RuntimeException; |
25 | use Wikimedia\Services\NoSuchServiceException; |
26 | |
27 | class Hooks implements \MediaWiki\Installer\Hook\LoadExtensionSchemaUpdatesHook { |
28 | |
29 | public static function registerExtension() { |
30 | require_once __DIR__ . '/defines.php'; |
31 | |
32 | global $wgNamespaceContentModels; |
33 | $wgNamespaceContentModels[ NS_MAIN ] = CONTENT_MODEL_ZOBJECT; |
34 | |
35 | global $wgNamespaceProtection; |
36 | $wgNamespaceProtection[ NS_MAIN ] = [ 'wikilambda-edit', 'wikilambda-create' ]; |
37 | |
38 | // (T267232) Prevent ZObject pages from being transcluded; sadly this isn't available as |
39 | // an extension.json attribute as of yet. |
40 | global $wgNonincludableNamespaces; |
41 | $wgNonincludableNamespaces[] = NS_MAIN; |
42 | } |
43 | |
44 | /** |
45 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdates |
46 | * |
47 | * @param DatabaseUpdater $updater DatabaseUpdater subclass |
48 | */ |
49 | public function onLoadExtensionSchemaUpdates( $updater ) { |
50 | $db = $updater->getDB(); |
51 | $type = $db->getType(); |
52 | $dir = __DIR__ . '/../sql'; |
53 | |
54 | if ( !in_array( $type, [ 'mysql', 'sqlite', 'postgres' ] ) ) { |
55 | wfWarn( "Database type '$type' is not supported by the WikiLambda extension." ); |
56 | return; |
57 | } |
58 | |
59 | $tables = [ |
60 | 'zobject_labels', |
61 | 'zobject_label_conflicts', |
62 | 'zobject_function_join', |
63 | 'ztester_results', |
64 | 'zlanguages' |
65 | ]; |
66 | |
67 | foreach ( $tables as $key => $table ) { |
68 | $updater->addExtensionTable( 'wikilambda_' . $table, "$dir/$type/table-$table.sql" ); |
69 | } |
70 | |
71 | // Database updates: |
72 | // (T285368) Add primary label field to labels table |
73 | $updater->addExtensionField( |
74 | 'wikilambda_zobject_labels', |
75 | 'wlzl_label_primary', |
76 | "$dir/$type/patch-add-primary-label-field.sql" |
77 | ); |
78 | |
79 | // (T262089) Add return type field to labels table |
80 | $updater->addExtensionField( |
81 | 'wikilambda_zobject_labels', |
82 | 'wlzl_return_type', |
83 | "$dir/$type/patch-add-return-type-field.sql" |
84 | ); |
85 | |
86 | $updater->addExtensionUpdate( [ [ self::class, 'createInitialContent' ] ] ); |
87 | } |
88 | |
89 | /** |
90 | * Return path of data definition JSON files. |
91 | * |
92 | * @return string |
93 | */ |
94 | protected static function getDataPath() { |
95 | return dirname( __DIR__ ) . '/function-schemata/data/definitions/'; |
96 | } |
97 | |
98 | /** |
99 | * Installer/Updater callback to create the initial "system" ZObjects on any installation. This |
100 | * is a callback so that it runs after the tables have been created/updated. |
101 | * |
102 | * @param DatabaseUpdater $updater |
103 | * @param bool $overwrite If true, overwrites the content, else skips if present |
104 | */ |
105 | public static function createInitialContent( DatabaseUpdater $updater, $overwrite = false ) { |
106 | // Ensure that the extension is set up (namespace is defined) even when running in update.php outside of MW. |
107 | self::registerExtension(); |
108 | |
109 | $contentHandler = MediaWikiServices::getInstance()->getContentHandlerFactory(); |
110 | if ( !$contentHandler->isDefinedModel( CONTENT_MODEL_ZOBJECT ) ) { |
111 | if ( method_exists( $contentHandler, 'defineContentHandler' ) ) { |
112 | // @phan-suppress-next-line PhanUndeclaredMethod this apparently phan doesn't take the hint above |
113 | $contentHandler->defineContentHandler( CONTENT_MODEL_ZOBJECT, ZObjectContentHandler::class ); |
114 | } else { |
115 | throw new ConfigException( 'WikiLambda content model is not registered and we cannot inject it.' ); |
116 | } |
117 | } |
118 | |
119 | // Note: Hard-coding the English version for messages as this can run without a Context and so no language set. |
120 | $creatingUserName = wfMessage( 'wikilambda-systemuser' )->inLanguage( 'en' )->text(); |
121 | $creatingUser = User::newSystemUser( $creatingUserName, [ 'steal' => true ] ); |
122 | // We use wikilambda-bootstrapupdatingeditsummary in maintenance scripts when updating. |
123 | $creatingComment = wfMessage( 'wikilambda-bootstrapcreationeditsummary' )->inLanguage( 'en' )->text(); |
124 | |
125 | if ( !$creatingUser ) { |
126 | // Something went wrong, give up. |
127 | return; |
128 | } |
129 | |
130 | $initialDataToLoadPath = static::getDataPath(); |
131 | |
132 | $dependenciesFile = file_get_contents( $initialDataToLoadPath . 'dependencies.json' ); |
133 | if ( $dependenciesFile === false ) { |
134 | throw new RuntimeException( |
135 | 'Could not load dependencies file from function-schemata sub-repository of the WikiLambda extension.' |
136 | . ' Have you initiated & fetched it? Try `git submodule update --init --recursive`.' |
137 | ); |
138 | } |
139 | $dependencies = json_decode( $dependenciesFile, true ); |
140 | |
141 | $initialDataToLoadListing = array_filter( |
142 | scandir( $initialDataToLoadPath ), |
143 | static function ( $key ) { |
144 | return (bool)preg_match( '/^Z\d+\.json$/', $key ); |
145 | } |
146 | ); |
147 | |
148 | // Naturally sort, so Z2/Persistent Object gets created before others |
149 | natsort( $initialDataToLoadListing ); |
150 | |
151 | $inserted = []; |
152 | foreach ( $initialDataToLoadListing as $filename ) { |
153 | static::insertContentObject( |
154 | $updater, |
155 | $filename, |
156 | $dependencies, |
157 | $creatingUser, |
158 | $creatingComment, |
159 | $overwrite, |
160 | $inserted |
161 | ); |
162 | } |
163 | } |
164 | |
165 | /** |
166 | * Inserts into the database the ZObject found in a given filename of the data directory. First checks |
167 | * whether the ZObject has any dependencies, according to the dependencies.json manifest file, and if so, |
168 | * inserts all the dependencies before trying the current ZObject. |
169 | * |
170 | * Runs in a static context and so can't be part of the normal code in ZObjectStore. |
171 | * |
172 | * @param DatabaseUpdater $updater |
173 | * @param string $filename |
174 | * @param array $dependencies |
175 | * @param User $user |
176 | * @param string $comment |
177 | * @param bool $overwrite |
178 | * @param string[] &$inserted |
179 | * @param string[] $track |
180 | * @return bool Has successfully inserted the content object |
181 | */ |
182 | protected static function insertContentObject( |
183 | $updater, $filename, $dependencies, $user, $comment, $overwrite = false, &$inserted = [], $track = [] |
184 | ) { |
185 | $initialDataToLoadPath = static::getDataPath(); |
186 | $updateRowName = "create WikiLambda initial content - $filename"; |
187 | |
188 | $langReg = ZLangRegistry::singleton(); |
189 | $typeReg = ZTypeRegistry::singleton(); |
190 | |
191 | // Check dependencies |
192 | $zid = substr( $filename, 0, -5 ); |
193 | |
194 | if ( array_key_exists( $zid, $dependencies ) ) { |
195 | $deps = $dependencies[ $zid ]; |
196 | foreach ( $deps as $dep ) { |
197 | if ( |
198 | // Avoid circular dependencies |
199 | !in_array( $dep, $track ) |
200 | && !$langReg->isZidCached( $dep ) |
201 | && !$typeReg->isZidCached( $dep ) |
202 | ) { |
203 | // Call recursively till all dependencies have been added |
204 | $success = self::insertContentObject( |
205 | $updater, |
206 | "$dep.json", |
207 | $dependencies, |
208 | $user, |
209 | $comment, |
210 | $overwrite, |
211 | $inserted, |
212 | array_merge( $track, (array)$dep ) |
213 | ); |
214 | // If any of the dependencies fail, we desist on the current insertion |
215 | if ( !$success ) { |
216 | return false; |
217 | } |
218 | } |
219 | } |
220 | } |
221 | |
222 | $zid = substr( $filename, 0, -5 ); |
223 | $title = Title::newFromText( $zid, NS_MAIN ); |
224 | $services = MediaWikiServices::getInstance(); |
225 | $page = $services->getWikiPageFactory()->newFromTitle( $title ); |
226 | |
227 | // If we don't want to overwrite the ZObjects, and if Zid has already been inserted, |
228 | // just purge the page to update secondary data and return true |
229 | if ( |
230 | ( $overwrite && in_array( $zid, $inserted ) ) || |
231 | ( !$overwrite && $updater->updateRowExists( $updateRowName ) ) |
232 | ) { |
233 | $page->doPurge(); |
234 | return true; |
235 | } |
236 | |
237 | $data = file_get_contents( $initialDataToLoadPath . $filename ); |
238 | if ( !$data ) { |
239 | // something went wrong, give up. |
240 | $updater->output( "\tâ Unable to load file contents for {$title->getPrefixedText()}.\n" ); |
241 | return false; |
242 | } |
243 | |
244 | try { |
245 | $content = ZObjectContentHandler::makeContent( $data, $title ); |
246 | } catch ( ZErrorException $e ) { |
247 | $updater->output( "\tâ Unable to make a ZObject for {$title->getPrefixedText()}.\n" ); |
248 | return false; |
249 | } |
250 | |
251 | // If we're in the installer, it won't have registered our extension's services yet. |
252 | try { |
253 | $services->get( 'WikiLambdaZObjectStore' ); |
254 | } catch ( NoSuchServiceException $e ) { |
255 | |
256 | $zObjectStore = new ZObjectStore( |
257 | $services->getDBLoadBalancerFactory(), |
258 | $services->getTitleFactory(), |
259 | $services->getWikiPageFactory(), |
260 | $services->getRevisionStore(), |
261 | $services->getUserGroupManager(), |
262 | LoggerFactory::getInstance( 'WikiLambda' ) |
263 | ); |
264 | $services->defineService( |
265 | 'WikiLambdaZObjectStore', |
266 | static function () use ( $zObjectStore ) { |
267 | return $zObjectStore; |
268 | } |
269 | ); |
270 | } |
271 | |
272 | $pageUpdater = $page->newPageUpdater( $user ); |
273 | $pageUpdater->setContent( SlotRecord::MAIN, $content ); |
274 | $pageUpdater->setRcPatrolStatus( RecentChange::PRC_PATROLLED ); |
275 | |
276 | $pageUpdater->saveRevision( |
277 | CommentStoreComment::newUnsavedComment( $comment ?? '' ), |
278 | EDIT_AUTOSUMMARY | EDIT_NEW |
279 | ); |
280 | |
281 | if ( $pageUpdater->wasSuccessful() ) { |
282 | array_push( $inserted, $zid ); |
283 | $updater->insertUpdateRow( $updateRowName ); |
284 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
285 | // Don't log this during unit testing, quibble thinks it means we're broken. |
286 | $updater->output( "\tSuccessfully created {$title->getPrefixedText()}.\n" ); |
287 | } |
288 | } else { |
289 | $firstError = $pageUpdater->getStatus()->getErrors()[0]; |
290 | $error = wfMessage( $firstError[ 'message' ], $firstError[ 'params' ] ); |
291 | $updater->output( "\tâ Unable to make a page for {$title->getPrefixedText()}: $error\n" ); |
292 | } |
293 | |
294 | return $pageUpdater->wasSuccessful(); |
295 | } |
296 | |
297 | } |