Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReloadBuiltinData
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 6
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 reloadBuiltinSafe
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 reloadBuiltinForce
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 deleteZObjectUnsafe
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getAllBuiltinZids
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * WikiLambda reloadBuiltinData maintenance script
4 *
5 * Reloads all builtin data into the database. If the builtins have been updated manually,
6 * this script erases all the updates, restoring the data to their first state in the files.
7 *
8 * @file
9 * @ingroup Extensions
10 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
11 * @license MIT
12 */
13
14use MediaWiki\Extension\WikiLambda\Hooks;
15use MediaWiki\Extension\WikiLambda\ZObjectStore;
16use MediaWiki\Logger\LoggerFactory;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Page\DeletePageFactory;
19use MediaWiki\Page\WikiPageFactory;
20use MediaWiki\Title\TitleFactory;
21
22$IP = getenv( 'MW_INSTALL_PATH' );
23if ( $IP === false ) {
24    $IP = __DIR__ . '/../../..';
25}
26require_once "$IP/maintenance/Maintenance.php";
27
28class ReloadBuiltinData extends Maintenance {
29
30    // TODO (T335418): Magical future of having changes re-apply
31
32    /**
33     * @var ZObjectStore
34     */
35    private $zObjectStore = null;
36
37    /**
38     * @var TitleFactory
39     */
40    protected $titleFactory;
41
42    /**
43     * @var WikiPageFactory
44     */
45    protected $wikiPageFactory;
46
47    /**
48     * @var DeletePageFactory
49     */
50    protected $deletePageFactory;
51
52    /**
53     * @inheritDoc
54     */
55    public function __construct() {
56        parent::__construct();
57        $this->requireExtension( 'WikiLambda' );
58        $this->addDescription( 'Restores saved ZObjects with the updated builtin ZObjects from the data directory' );
59
60        $this->addOption(
61            'force',
62            'Forces the reload by clearing all secondary data and doing '
63                . 'an orderly insertion in case a normal reload fails',
64            false,
65            false
66        );
67
68        $this->addOption(
69            'clear',
70            'Clears all the non built-in ZObjects. The "--clear" flag can only '
71                . 'be used along with the "--force" flag.',
72            false,
73            false
74        );
75    }
76
77    /**
78     * @inheritDoc
79     */
80    public function execute() {
81        // Construct the ZObjectStore, because ServiceWiring hasn't run
82        $services = MediaWikiServices::getInstance();
83        $this->titleFactory = $services->getTitleFactory();
84        $this->wikiPageFactory = $services->getWikiPageFactory();
85        $this->deletePageFactory = $services->getDeletePageFactory();
86        $this->zObjectStore = new ZObjectStore(
87            $services->getDBLoadBalancerFactory(),
88            $services->getTitleFactory(),
89            $services->getWikiPageFactory(),
90            $services->getRevisionStore(),
91            $services->getUserGroupManager(),
92            LoggerFactory::getInstance( 'WikiLambda' )
93        );
94
95        $force = $this->getOption( 'force' );
96        $clear = $this->getOption( 'clear' );
97
98        if ( $force ) {
99            $this->reloadBuiltinForce( $clear === 1 );
100        } else {
101            if ( $clear ) {
102                // We could simply call reloadBuiltinForce( true ) if the user calls the script
103                // with the flag --clear, but this is just to make sure that the user is aware
104                // of the process occurring, as the force option means total secondary tables being
105                // wiped.
106                $this->output( 'The flag "--clear" should only be used along with the flag "--force"' );
107                $this->output( "\n" );
108            } else {
109                $this->reloadBuiltinSafe();
110            }
111        }
112    }
113
114    /**
115     * Re-inserts all the builtin ZObject available in data/definitions.
116     * This will not work as an initial data load, and might fail when there's
117     * complex migrations and some objects become not valid. In such case,
118     * one must run the script with the "--force" flag.
119     */
120    private function reloadBuiltinSafe() {
121        $initialDataToLoadPath = dirname( __DIR__ ) . '/function-schemata/data/definitions/';
122        $initialDataToLoadListing = array_filter(
123            scandir( $initialDataToLoadPath ),
124            static function ( $key ) {
125                return (bool)preg_match( '/^Z\d+\.json$/', $key );
126            }
127        );
128
129        $creatingComment = wfMessage( 'wikilambda-bootstrapupdatingeditsummary' )->inLanguage( 'en' )->text();
130
131        // Naturally sort, so Z2 gets created before Z12 etc.
132        natsort( $initialDataToLoadListing );
133
134        foreach ( $initialDataToLoadListing as $filename ) {
135            $zid = substr( $filename, 0, -5 );
136            $data = file_get_contents( $initialDataToLoadPath . $filename );
137
138            if ( !$data ) {
139                // Something went wrong, give up.
140                return;
141            }
142
143            // And we update the data
144            $response = $this->zObjectStore->updateZObjectAsSystemUser(
145                /* String zid */ $zid,
146                /* String content */ $data,
147                /* Edit summary */ $creatingComment,
148                /* Update flags */ EDIT_UPDATE
149            );
150
151            if ( $response->isOK() ) {
152                $this->output( "Updated $zid \n" );
153            } else {
154                $this->output( "Problem updating $zid \n" );
155                $this->output( $response->getErrors() );
156                $this->output( "\n" );
157            }
158        }
159    }
160
161    /**
162     * Re-inserts all the builtin ZObject available in data/definitions.
163     * This method will make sure that the insertion ends successfully, by
164     * taking drastic measures. It will first clear all the secondary tables
165     * to avoid possible conflicts, and then will call the createInitialContent
166     * hook so that the insertions happen in the right order.
167     *
168     * @param bool $clear Whether to also clear the non-builtin objects
169     */
170    private function reloadBuiltinForce( $clear ) {
171        // 1. Get builtin and custom Zid arrays
172        $allZids = $this->zObjectStore->fetchAllZids();
173        $builtinZids = self::getAllBuiltinZids();
174        $customZids = array_filter(
175            $allZids, static function ( $zid ) use ( $builtinZids ) {
176                return !in_array( $zid, $builtinZids );
177            }
178        );
179
180        if ( $clear ) {
181            $user = User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] );
182
183            // 2.a. Delete all non-builtin ZObjects
184            foreach ( $customZids as $zid ) {
185                $status = $this->deleteZObjectUnsafe(
186                    $zid,
187                    $user,
188                    'Maintenance script reloadBuiltinData --force --clear'
189                );
190                if ( $status->isOK() ) {
191                    $this->output( "Deleted custom ZObject $zid \n" );
192                } else {
193                    $this->output( "Problem deleting custom ZObject $zid \n" );
194                    $this->output( var_export( $status->getErrors(), true ) );
195                    $this->output( "\n" );
196                    return;
197                }
198            }
199            // 2.b. Clear all secondary tables
200            $this->zObjectStore->clearLabelsSecondaryTables();
201            $this->zObjectStore->clearFunctionsSecondaryTables();
202            $this->zObjectStore->clearTesterResultsSecondaryTables();
203            $this->zObjectStore->clearLanguageCacheSecondaryTables();
204        } else {
205            // 2. Clear only builtin zids from secondary tables
206            $this->zObjectStore->deleteFromLabelsSecondaryTables( $builtinZids );
207            $this->zObjectStore->deleteFromFunctionsSecondaryTables( $builtinZids );
208            $this->zObjectStore->deleteFromTesterResultsSecondaryTables( $builtinZids );
209            $this->zObjectStore->deleteFromLanguageCacheSecondaryTables( $builtinZids );
210        }
211
212        // 3. Call Hooks:createInitialContent
213        $updater = DatabaseUpdater::newForDB( $this->getDB( DB_PRIMARY ), true, $this );
214        Hooks::createInitialContent( $updater, true );
215    }
216
217    /**
218     * Deletes a ZObject if the user is allowed to do so. This method is only
219     * for its use by reloadBuiltinData with the --force and --clear flags.
220     *
221     * @param string $zid
222     * @param User $user
223     * @param string $reason
224     * @return StatusValue
225     */
226    private function deleteZObjectUnsafe( $zid, $user, $reason = '' ): StatusValue {
227        $title = $this->titleFactory->newFromText( $zid, NS_MAIN );
228        if ( !( $title instanceof Title ) ) {
229            return StatusValue::newFatal( 'wikilambda-invalidzobjecttitle' );
230        }
231        $page = $this->wikiPageFactory->newFromTitle( $title );
232        $deletePage = $this->deletePageFactory->newDeletePage( $page, $user );
233        return $deletePage->deleteUnsafe( $reason );
234    }
235
236    /**
237     * Returns the array of built-in ZIDS
238     *
239     * @return array
240     */
241    private static function getAllBuiltinZids() {
242        $dataPath = dirname( __DIR__ ) . '/function-schemata/data/definitions/';
243        $filenames = array_filter(
244            scandir( $dataPath ),
245            static function ( $key ) {
246                return (bool)preg_match( '/^Z\d+\.json$/', $key );
247            }
248        );
249        $zids = array_map(
250            static function ( $filename ) {
251                return substr( $filename, 0, -5 );
252            },
253            $filenames
254        );
255        return $zids;
256    }
257}
258
259$maintClass = ReloadBuiltinData::class;
260require_once RUN_MAINTENANCE_IF_MAIN;