Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 137
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
LoadJsonDump
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 3
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
306
 makeEdit
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3/**
4 * WikiLambda loadJsonDump maintenance script
5 *
6 * Loads specified dump of objects from production
7 *
8 * @file
9 * @ingroup Extensions
10 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
11 * @license MIT
12 */
13
14use MediaWiki\Extension\WikiLambda\ZErrorException;
15use MediaWiki\Extension\WikiLambda\ZObjectStore;
16use MediaWiki\Logger\LoggerFactory;
17use MediaWiki\Maintenance\Maintenance;
18use MediaWiki\Title\Title;
19use MediaWiki\Title\TitleFactory;
20
21$IP = getenv( 'MW_INSTALL_PATH' );
22if ( $IP === false ) {
23    $IP = __DIR__ . '/../../..';
24}
25require_once "$IP/maintenance/Maintenance.php";
26
27class LoadJsonDump extends Maintenance {
28
29    /**
30     * @inheritDoc
31     */
32    public function __construct() {
33        parent::__construct();
34        $this->requireExtension( 'WikiLambda' );
35        $this->addDescription( 'Loads all the latest versions of the objects located in the specified directory' );
36
37        $this->addOption(
38            'dir',
39            'Directory name of the wikifunctions dump',
40            false,
41            true
42        );
43
44        $this->addOption(
45            'zid',
46            'Particular zid to push',
47            false,
48            true
49        );
50
51        $this->addOption(
52            'from',
53            'Loads the objects from a lower range. Must be used along with "--to". E.g. "--from Z100 --to Z200"',
54            false,
55            true
56        );
57
58        $this->addOption(
59            'to',
60            'Loads the objects till an upper range. Must be used along with "--from". E.g. "--from Z100 --to Z200"',
61            false,
62            true
63        );
64
65        $this->addOption(
66            'refresh',
67            'If present, inserts all files, even the ones that loaded during a previous run and were consequently'
68            . ' renamed as "<zid>.<revision>.done.json"',
69        );
70    }
71
72    /**
73     * @inheritDoc
74     */
75    public function execute() {
76        // Construct the ZObjectStore, because ServiceWiring hasn't run
77        $services = $this->getServiceContainer();
78        $titleFactory = $services->getTitleFactory();
79        $zObjectStore = new ZObjectStore(
80            $services->getConnectionProvider(),
81            $services->getTitleFactory(),
82            $services->getWikiPageFactory(),
83            $services->getRevisionStore(),
84            $services->getUserGroupManager(),
85            LoggerFactory::getInstance( 'WikiLambda' ),
86        );
87
88        // Dump path:
89        $dumpDir = $this->getOption( 'dir' );
90        if ( !$dumpDir ) {
91            $this->fatalError( "Please specify the folder where the cached ZObjects are stored. E.g.:\n"
92                . "docker compose exec mediawiki php extensions/WikiLambda/maintenance/loadJsonDump.php "
93                . "--dir zobjectcache" );
94        }
95        $path = dirname( __DIR__ ) . '/' . $dumpDir;
96
97        // Dump only one zid:
98        $pushZid = $this->getOption( 'zid' );
99
100        // Dump zids in a given range:
101        $pushZidsFrom = $this->getOption( 'from' );
102        $pushZidsTo = $this->getOption( 'to' );
103
104        // Whether or not to refresh already added files
105        $refresh = $this->hasOption( 'refresh' );
106
107        // Get data files
108        // Load Z0.json
109        $indexFile = file_get_contents( "$path/Z0.json" );
110        if ( $indexFile === false ) {
111            $this->fatalError( "Could not load Z0.json guide file.\n"
112                . "The directory must contain the objects downloaded with "
113                . "https://gitlab.wikimedia.org/repos/abstract-wiki/wikifunctions-content-download" );
114        }
115        $index = json_decode( $indexFile, true );
116        $index = is_array( $index ) ? $index : [];
117
118        $success = 0;
119        $errors = [];
120
121        // If inserting only from a zid, replace index array with only the zids from that one
122        if ( $pushZidsFrom && $pushZidsTo ) {
123            $lowerZid = intval( substr( $pushZidsFrom, 1 ) );
124            $upperZid = intval( substr( $pushZidsTo, 1 ) );
125            $index = array_filter( $index, static function ( $value, $key ) use ( $lowerZid, $upperZid ) {
126                $zid = intval( substr( $key, 1 ) );
127                return $zid >= $lowerZid && $zid <= $upperZid;
128            }, ARRAY_FILTER_USE_BOTH );
129        }
130
131        // If only one to insertZid, replace index array with zid => revision, or exit early if not found
132        if ( $pushZid ) {
133            if ( array_key_exists( $pushZid, $index ) ) {
134                $revision = $index[ $pushZid ];
135                $index = [ $pushZid => $revision ];
136            } else {
137                $this->fatalError( "The Zid provided doesn't exist in the directory" );
138            }
139        }
140
141        // Go through all the zid-version index and push one by one
142        foreach ( $index as $zid => $revision ) {
143            $filename = "$zid.$revision.json";
144            $doneFilename = "$zid.$revision.done.json";
145
146            // If already processed, skip
147            if ( file_exists( "$path/$doneFilename" ) ) {
148                if ( $refresh ) {
149                    // Reinsert using the already processed file
150                    $this->output( "$filename was already inserted. Reinserting.\n" );
151                    $filename = $doneFilename;
152                } else {
153                    $this->output( "$filename was already inserted. Skipping.\n" );
154                    continue;
155                }
156            }
157
158            $response = $this->makeEdit( $titleFactory, $zObjectStore, $zid, $path, $filename );
159
160            if ( $response->isOK ) {
161                $success++;
162                $this->output( $response->message );
163
164                // Rename file after successful insertion (if needed)
165                if ( $filename !== $doneFilename ) {
166                    rename( "$path/$filename", "$path/$doneFilename" );
167                }
168            } else {
169                // Print error immediately, but also save it for summary
170                $this->error( $response->message );
171                $errors[] = $response->message;
172            }
173        }
174
175        // Print summary:
176        // * n objects successfully created
177        // * n objects failed
178        // * details of all failures
179        $this->output( "\nDONE!\n" );
180        if ( $success > 0 ) {
181            $this->output( "$success objects were created or updated successfully.\n" );
182        }
183        if ( count( $errors ) > 0 ) {
184            $this->error( count( $errors ) . " objects failed on creation or update.\n" );
185
186            $this->error( "Failure details:\n" );
187            foreach ( $errors as $error ) {
188                $this->error( "$error\n" );
189            }
190        }
191    }
192
193    /**
194     * Pushes the object from the given filename.
195     *
196     * @param TitleFactory $titleFactory
197     * @param ZObjectStore $zObjectStore
198     * @param string $zid
199     * @param string $path
200     * @param string $filename
201     * @return \stdClass with isOK and message properties
202     */
203    private function makeEdit(
204        TitleFactory $titleFactory,
205        ZObjectStore $zObjectStore,
206        string $zid,
207        string $path,
208        string $filename
209    ) {
210        // Prepare return object
211        $return = (object)[
212            'isOK' => true,
213            'message' => ''
214        ];
215
216        $data = file_get_contents( "$path/$filename" );
217
218        if ( !$data ) {
219            $return->isOK = false;
220            $return->message = "The file $filename was not found in the path $path\n";
221            return $return;
222        }
223
224        $title = $titleFactory->newFromText( $zid, NS_MAIN );
225
226        if ( !( $title instanceof Title ) ) {
227            $return->isOK = false;
228            $return->message = "The ZObject $zid cannot be loaded: invalid name\n";
229            return $return;
230        }
231
232        $creating = !$title->exists();
233        $summary = wfMessage(
234            $creating
235                ? 'wikilambda-bootstrapcreationeditsummary'
236                : 'wikilambda-bootstrapupdatingeditsummary'
237        )->inLanguage( 'en' )->text();
238
239        // We create or update the ZObject
240        try {
241            $zObjectStore->pushZObject( $zid, $data, $summary );
242            $return->message = ( $creating ? "Created" : "Updated" ) . " $zid\n";
243        } catch ( ZErrorException $e ) {
244            $return->isOK = false;
245            $return->message = "❌ Problem " . ( $creating ? 'creating' : 'updating' ) . " $zid:\n"
246                . $e->getMessage() . "\n"
247                . $e->getZErrorMessage() . "\n";
248        } catch ( Exception $e ) {
249            $return->isOK = false;
250            $return->message = "❌ Problem " . ( $creating ? 'creating' : 'updating' ) . " $zid:\n"
251                . $e->getMessage() . "\n"
252                . $e->getTraceAsString() . "\n";
253        }
254
255        return $return;
256    }
257}
258
259$maintClass = LoadJsonDump::class;
260require_once RUN_MAINTENANCE_IF_MAIN;