Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.27% covered (warning)
58.27%
81 / 139
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
58.27% covered (warning)
58.27%
81 / 139
40.00% covered (danger)
40.00%
2 / 5
79.96
0.00% covered (danger)
0.00%
0 / 1
 registerExtension
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 onLoadExtensionSchemaUpdates
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
12
 getDataPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createInitialContent
78.38% covered (warning)
78.38%
29 / 37
0.00% covered (danger)
0.00%
0 / 1
6.36
 insertContentObject
66.20% covered (warning)
66.20%
47 / 71
0.00% covered (danger)
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
11namespace MediaWiki\Extension\WikiLambda;
12
13use MediaWiki\CommentStore\CommentStoreComment;
14use MediaWiki\Config\ConfigException;
15use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry;
16use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
17use MediaWiki\Installer\DatabaseUpdater;
18use MediaWiki\Logger\LoggerFactory;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Revision\SlotRecord;
21use MediaWiki\Title\Title;
22use MediaWiki\User\User;
23use RecentChange;
24use RuntimeException;
25use Wikimedia\Services\NoSuchServiceException;
26
27class 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}