Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.48% covered (warning)
65.48%
220 / 336
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
LoadPreDefinedObject
66.67% covered (warning)
66.67%
220 / 330
11.11% covered (danger)
11.11%
1 / 9
478.81
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
1 / 1
1
 execute
91.30% covered (success)
91.30%
84 / 92
0.00% covered (danger)
0.00%
0 / 1
17.19
 makeEdit
53.49% covered (warning)
53.49%
23 / 43
0.00% covered (danger)
0.00%
0 / 1
41.76
 mergeData
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
380
 resolveConflicts
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 findPropAndSet
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 printDiff
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 undoChange
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getOptions
94.92% covered (success)
94.92%
56 / 59
0.00% covered (danger)
0.00%
0 / 1
31.13
1<?php
2
3/**
4 * WikiLambda loadPreDefinedObject maintenance script
5 *
6 * Loads specified pre-defined Object, range of Objects, or all pre-defined Objects into the database.
7 *
8 * @file
9 * @ingroup Extensions
10 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
11 * @license MIT
12 */
13
14namespace MediaWiki\Extension\WikiLambda\Maintenance;
15
16use Exception;
17use MediaWiki\Extension\WikiLambda\Diff\ZObjectDiffer;
18use MediaWiki\Extension\WikiLambda\ZErrorException;
19use MediaWiki\Extension\WikiLambda\ZObjectContent\ZObjectContent;
20use MediaWiki\Extension\WikiLambda\ZObjectStore;
21use MediaWiki\Json\FormatJson;
22use MediaWiki\Logger\LoggerFactory;
23use MediaWiki\Maintenance\Maintenance;
24use MediaWiki\Title\Title;
25use MediaWiki\Title\TitleFactory;
26use RuntimeException;
27
28$IP = getenv( 'MW_INSTALL_PATH' );
29if ( $IP === false ) {
30    $IP = __DIR__ . '/../../..';
31}
32require_once "$IP/maintenance/Maintenance.php";
33
34class LoadPreDefinedObject extends Maintenance {
35
36    /**
37     * @inheritDoc
38     */
39    public function __construct() {
40        parent::__construct();
41        $this->requireExtension( 'WikiLambda' );
42        $this->addDescription( 'Loads a specified pre-defined Object into the database' );
43
44        $this->addOption(
45            'zid',
46            'Loads the requested ZID. E.g. "--zid Z100"',
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            'all',
67            'Loads all built-in objects, from Z1 to Z9999.',
68            false,
69            false
70        );
71
72        $this->addOption(
73            'force',
74            'Forces the load even if the Object already exists (clears the Object)',
75            false,
76            false
77        );
78
79        $this->addOption(
80            'merge',
81            'Updates the objects but keeps the multilingual data untouched',
82            false,
83            false
84        );
85
86        $this->addOption(
87            'builtin',
88            'On merge conflicts, automatically defaults to restoring builtin values',
89            false,
90            false
91        );
92
93        $this->addOption(
94            'current',
95            'On merge conflicts, automatically defaults to keeping the current values',
96            false,
97            false
98        );
99
100        $this->addOption(
101            'wait',
102            'Sleeps the given time (in ms) between inserts',
103            false,
104            true
105        );
106    }
107
108    /**
109     * @inheritDoc
110     */
111    public function execute() {
112        // Validate and collect arguments
113        [
114            $all,
115            $from,
116            $to,
117            $force,
118            $merge,
119            $builtin,
120            $current,
121            $wait
122        ] = $this->getOptions();
123
124        // Construct the ZObjectStore, because ServiceWiring hasn't run
125        $services = $this->getServiceContainer();
126        $titleFactory = $services->getTitleFactory();
127        $zObjectStore = new ZObjectStore(
128            $services->getConnectionProvider(),
129            $services->getTitleFactory(),
130            $services->getWikiPageFactory(),
131            $services->getRevisionStore(),
132            $services->getUserGroupManager(),
133            LoggerFactory::getInstance( 'WikiLambda' ),
134        );
135
136        // Base path:
137        $path = dirname( __DIR__ ) . '/function-schemata/data/definitions/';
138
139        // Get dependencies file
140        $dependencies = [];
141        $dependenciesFile = file_get_contents( $path . 'dependencies.json' );
142        if ( $dependenciesFile === false ) {
143            $this->fatalError(
144                'Could not load dependencies file from function-schemata sub-repository of the WikiLambda extension.'
145                . ' Have you initiated & fetched it? Try `git submodule update --init --recursive`.'
146            );
147        }
148        $dependenciesIndex = json_decode( $dependenciesFile, true );
149
150        // Get data files
151        $initialDataToLoadListing = array_filter(
152            scandir( $path ),
153            static function ( $key ) use ( $from, $to ) {
154                if ( preg_match( '/^Z(\d+)\.json$/', $key, $match ) ) {
155                    if ( $match[1] >= $from && $match[1] <= $to ) {
156                        return true;
157                    }
158                }
159                return false;
160            }
161        );
162
163        // Get zids to load
164        $zidsToLoad = array_map(
165            static function ( string $filename ): string {
166                return substr( $filename, 0, -5 );
167            },
168            $initialDataToLoadListing
169        );
170
171        // Naturally sort, so Z2 gets created before Z12 etc.
172        natsort( $zidsToLoad );
173
174        $success = 0;
175        $unchanged = 0;
176        $failure = 0;
177        $skipped = 0;
178
179        foreach ( $zidsToLoad as $zid ) {
180            // Gather dependencies
181            $dependencies = array_merge( $dependencies, $dependenciesIndex[ $zid ] ?? [] );
182
183            // Make edit
184            $response = $this->makeEdit(
185                $zid,
186                $path,
187                $titleFactory,
188                $zObjectStore,
189                $force,
190                $merge,
191                $builtin,
192                $current
193            );
194
195            // Wait requested ms
196            usleep( $wait * 1000 );
197
198            switch ( $response ) {
199                case 1:
200                    $success++;
201                    break;
202
203                case 2:
204                    $unchanged++;
205                    break;
206
207                case -1:
208                    $failure++;
209                    break;
210
211                case 0:
212                    $skipped++;
213                    break;
214
215                default:
216                    throw new RuntimeException( 'Unrecognised return value!' );
217            }
218        }
219
220        $this->output( "\nDone!\n" );
221
222        if ( $success > 0 ) {
223            $this->output( "$success objects were created or updated successfully.\n" );
224        }
225
226        if ( $unchanged > 0 ) {
227            $this->output( "$unchanged objects were already up to date (no change).\n" );
228        }
229
230        if ( $skipped > 0 ) {
231            $this->output( "$skipped objects were skipped.\n" );
232        }
233
234        if ( $failure > 0 ) {
235            $this->fatalError( "$failure objects failed to create or update.\n" );
236        }
237
238        // Print dependency warning if one zid or a partial range were inserted
239        if ( !$all ) {
240            // Unique zids:
241            $dependencies = array_unique( $dependencies );
242            // Exclude inserted zids:
243            $dependencies = array_filter( $dependencies, static function ( string $item ) use ( $zidsToLoad ) {
244                return !in_array( $item, $zidsToLoad );
245            } );
246            // Sort naturally:
247            natsort( $dependencies );
248            // Output dependency notice:
249            if ( count( $dependencies ) > 0 ) {
250                $this->output( "\nMake sure the following dependencies are inserted and up to date:\n" );
251                $this->output( implode( ', ', $dependencies ) . "\n" );
252            }
253        }
254    }
255
256    /**
257     * Pushes the given object with the version available in the
258     * zobject builtin data definitions directory. If the object is
259     * already available, forces a full override or merges with the
260     * current data depending on the --force or --merge flags
261     *
262     * @param string $zid
263     * @param string $path
264     * @param TitleFactory $titleFactory
265     * @param ZObjectStore $zObjectStore
266     * @param bool $force
267     * @param bool $merge
268     * @param bool $builtin
269     * @param bool $current
270     * @return int 1=success, -1=failure, 0=skipped
271     */
272    private function makeEdit(
273        string $zid,
274        string $path,
275        TitleFactory $titleFactory,
276        ZObjectStore $zObjectStore,
277        bool $force,
278        bool $merge,
279        bool $builtin,
280        bool $current
281    ) {
282        $data = file_get_contents( $path . $zid . '.json' );
283        // If no data in builtins folder, return error
284        if ( !$data ) {
285            $this->error( 'The ZObject "' . $zid . '" was not found in the definitions folder.' );
286            return -1;
287        }
288
289        $title = $titleFactory->newFromText( $zid, NS_MAIN );
290        // If title is invalid, return error
291        if ( !( $title instanceof Title ) ) {
292            $this->error( 'The ZObject title "' . $zid . '" could not be loaded somehow; invalid name?' );
293            return -1;
294        }
295
296        $mergeSummary = '';
297        $creating = !$title->exists();
298        // If the object already exists:
299        if ( !$creating ) {
300            // Get current ZObjectContent, returns false if not found
301            $oldContent = $zObjectStore->fetchZObjectByTitle( $title );
302
303            // If merge flag is passed, merge builtin and current versions
304            if ( $merge && $oldContent ) {
305                '@phan-var ZObjectContent $oldContent';
306                // 1. Automatic merge; multilingual data, tests, implementations, etc.
307                $data = $this->mergeData( $oldContent, $data );
308                // 2. Supervised merge; check the diff and prompt user for confirmation
309                [ $data, $conflicts ] = $this->resolveConflicts( $zid, $oldContent, $data, $builtin, $current );
310                // Set summary with number of resolved conflicts (if any)
311                if ( $conflicts > 0 ) {
312                    $mergeSummary = "($conflicts conflicts)";
313                }
314            }
315
316            // If no merge and no force flags are passed, exit
317            if ( !$force && !$merge ) {
318                $this->error( 'The ZObject "' . $zid . '" already exists and --force or --merge were not set.' );
319                return 0;
320            }
321        }
322
323        $summary = wfMessage(
324            $creating
325                ? 'wikilambda-bootstrapcreationeditsummary'
326                : 'wikilambda-bootstrapupdatingeditsummary'
327        )->inLanguage( 'en' )->text();
328
329        // We create or update the ZObject
330        try {
331            $revisionCreated = $zObjectStore->pushZObject( $zid, $data, $summary );
332            if ( $revisionCreated ) {
333                $this->output( ( $creating ? 'Created' : 'Updated' ) . " $zid $mergeSummary\n" );
334                return 1;
335            }
336            // Null edit: the pushed data matched the current revision, so nothing changed.
337            $this->output( "Unchanged $zid $mergeSummary\n" );
338            return 2;
339        } catch ( ZErrorException $e ) {
340            $this->error( "Problem " . ( $creating ? 'creating' : 'updating' ) . " $zid:" );
341            $this->error( $e->getMessage() );
342            $this->error( $e->getZErrorMessage()->__toString() );
343            $this->error( "\n" );
344            return -1;
345        } catch ( Exception $e ) {
346            $this->error( "Problem " . ( $creating ? 'creating' : 'updating' ) . " $zid:" );
347            $this->error( $e->getMessage() );
348            $this->error( $e->getTraceAsString() );
349            $this->error( "\n" );
350            return -1;
351        }
352    }
353
354    /**
355     * Automatic merge of current object (old) and builtin version (new).
356     * This keeps all the data that we know we need to keep from the
357     * current stored object, which includes:
358     * - For every object: multilingual data
359     * - For functions: list of tests and implementations
360     * - For types: type functions (equality, validator, renderer, parser
361     *   and lists of converters from/to code)
362     *
363     * @param ZObjectContent $oldContent
364     * @param string $data
365     * @return string
366     */
367    private function mergeData( $oldContent, $data ) {
368        $parsedData = FormatJson::parse( $data );
369        $newObject = $parsedData->getValue();
370        $oldObject = $oldContent->getObject();
371
372        // 1. Keep whole Z2K3/Name key
373        $newObject->Z2K3 = $oldObject->Z2K3;
374
375        // 2. Keep whole Z2K4/Aliases key (if any)
376        if ( property_exists( $oldObject, 'Z2K4' ) ) {
377            $newObject->Z2K4 = $oldObject->Z2K4;
378        }
379
380        // 3. Keep whole Z2K5/Description key (if any)
381        if ( property_exists( $oldObject, 'Z2K5' ) ) {
382            $newObject->Z2K5 = $oldObject->Z2K5;
383        }
384
385        // 4. Keep type-specific content
386        $type = $oldObject->Z2K2->Z1K1;
387        switch ( $type ) {
388            // 4.a: For types:
389            case 'Z4':
390                // 4.a.1: For each key, keep whole content of Z3K3/Key label:
391                foreach ( $oldObject->Z2K2->Z4K2 as $index => $oldKey ) {
392                    // Skip benjamin array type item
393                    if ( $index === 0 ) {
394                        continue;
395                    }
396                    $newObject->Z2K2->Z4K2[ $index ]->Z3K3 = $oldKey->Z3K3;
397                }
398
399                // 4.a.2: Keep current validator/Z4K3, equality/Z4K4, renderer/Z4K5 and parser/Z4K6
400                if ( property_exists( $oldObject->Z2K2, 'Z4K3' ) ) {
401                    $newObject->Z2K2->Z4K3 = $oldObject->Z2K2->Z4K3;
402                }
403                if ( property_exists( $oldObject->Z2K2, 'Z4K4' ) ) {
404                    $newObject->Z2K2->Z4K4 = $oldObject->Z2K2->Z4K4;
405                }
406                if ( property_exists( $oldObject->Z2K2, 'Z4K5' ) ) {
407                    $newObject->Z2K2->Z4K5 = $oldObject->Z2K2->Z4K5;
408                }
409                if ( property_exists( $oldObject->Z2K2, 'Z4K6' ) ) {
410                    $newObject->Z2K2->Z4K6 = $oldObject->Z2K2->Z4K6;
411                }
412
413                // 4.a.3: Keep current converters (Z4K7 and Z4K8)
414                if ( property_exists( $oldObject->Z2K2, 'Z4K7' ) ) {
415                    $newObject->Z2K2->Z4K7 = $oldObject->Z2K2->Z4K7;
416                }
417                if ( property_exists( $oldObject->Z2K2, 'Z4K8' ) ) {
418                    $newObject->Z2K2->Z4K8 = $oldObject->Z2K2->Z4K8;
419                }
420
421                break;
422
423            // 4.b: For functions:
424            case 'Z8':
425                // 4.b.1: For each arg, keep whole content of Z17K3/Input label:
426                foreach ( $oldObject->Z2K2->Z8K1 as $index => $oldArg ) {
427                    // Skip benjamin array type item
428                    if ( $index === 0 ) {
429                        continue;
430                    }
431                    $newObject->Z2K2->Z8K1[ $index ]->Z17K3 = $oldArg->Z17K3;
432                }
433
434                // 4.b.2: Keep current list of tests/Z8K3
435                $newObject->Z2K2->Z8K3 = $oldObject->Z2K2->Z8K3;
436
437                // 4.b.3: Keep current list of implementations/Z8K4
438                $newObject->Z2K2->Z8K4 = $oldObject->Z2K2->Z8K4;
439
440                break;
441
442            // For error types: For each key, copy whole Z3K3 key
443            case 'Z50':
444                foreach ( $oldObject->Z2K2->Z50K1 as $index => $oldKey ) {
445                    // Skip benjamin array type item
446                    if ( $index === 0 ) {
447                        continue;
448                    }
449                    $newObject->Z2K2->Z50K1[ $index ]->Z3K3 = $oldKey->Z3K3;
450                }
451                break;
452
453            default:
454                break;
455        }
456
457        return FormatJson::encode( $newObject, true, FormatJson::UTF8_OK );
458    }
459
460    /**
461     * Supervised merge of current object (old) and builtin version (new).
462     * This tries to merge all other diffs that were not automatically merged
463     * during the mergeData step, and requests input from the user to keep
464     * the current version or restore the builtin value.
465     * The builtin and current flags will automatically run the script without
466     * requesting user input.
467     * Returns a list with the final object encoded as a string, and the count
468     * of resolved conflicts.
469     *
470     * @param string $zid
471     * @param ZObjectContent $oldContent
472     * @param string $data
473     * @param bool $builtin
474     * @param bool $current
475     * @return array list( string: $data, int: $conflicts )
476     */
477    private function resolveConflicts( $zid, $oldContent, $data, $builtin, $current ) {
478        $differ = new ZObjectDiffer();
479        $diffOps = $differ->doDiff(
480            /* oldValues: current content stored in the DB */
481            json_decode( json_encode( $oldContent->getObject() ), true ),
482            /* newValues: content from builtin data to restore */
483            json_decode( $data, true )
484        );
485
486        $flats = ZObjectDiffer::flattenDiff( $diffOps );
487        foreach ( $flats as $diff ) {
488            $restoreBuiltin = $builtin;
489            // If no --builtin or --current flags were passed, request interactive input
490            if ( !$builtin && !$current ) {
491                $this->printDiff( $zid, $diff );
492                $prompt = $this->prompt( '> Restore to builtin value? (y/n)', 'n' );
493                $restoreBuiltin = $prompt === 'y';
494            }
495            if ( !$restoreBuiltin ) {
496                $data = $this->undoChange( $data, $diff );
497            }
498        }
499
500        // Return new version and count of resolved conflicts
501        return [ $data, count( $flats ) ];
502    }
503
504    /**
505     * Walk the tree in depth following the keys passed in the path
506     * array, and set the new value when arrive to the leaf. If the
507     * new value is null, unset the key.
508     *
509     * @param array &$object reference to the associative array to mutate
510     * @param array $path array of keys to follow down the object
511     * @param string|array|null $newValue new value to set
512     */
513    private function findPropAndSet( &$object, $path, $newValue = null ) {
514        $head = array_shift( $path );
515        if ( count( $path ) === 0 ) {
516            if ( $newValue ) {
517                $object[ $head ] = $newValue;
518            } else {
519                unset( $object[ $head ] );
520            }
521        } else {
522            if ( isset( $object[ $head ] ) ) {
523                $this->findPropAndSet( $object[ $head ], $path, $newValue );
524            }
525        }
526    }
527
528    /**
529     * Print the details of the Diff to merge.
530     *
531     * @param string $zid
532     * @param array $diff
533     */
534    private function printDiff( $zid, $diff ) {
535        $type = $diff[ 'op' ]->getType();
536        $oldValue = ( $type === 'change' || $type === 'remove' ) ? $diff[ 'op' ]->getOldValue() : null;
537        $newValue = ( $type === 'change' || $type === 'add' ) ? $diff[ 'op' ]->getNewValue() : null;
538
539        $this->output( "> Conflict:\n" );
540        $this->output( "  | Zid: $zid\n" );
541        $this->output( "  | Path: " . implode( '.', $diff[ 'path' ] ) . "\n" );
542        $this->output( "  | Current value: " . json_encode( $oldValue ) . "\n" );
543        $this->output( "  | Builtin value: " . json_encode( $newValue ) . "\n" );
544    }
545
546    /**
547     * Restores the old value of the Diff operation.
548     *
549     * @param string $data
550     * @param array $diff
551     * @return string
552     */
553    private function undoChange( $data, $diff ) {
554        $newObject = json_decode( $data, true );
555        $path = $diff[ 'path' ];
556
557        $type = $diff[ 'op' ]->getType();
558        $oldValue = ( $type === 'change' || $type === 'remove' ) ? $diff[ 'op' ]->getOldValue() : null;
559
560        $this->findPropAndSet( $newObject, $path, $oldValue );
561
562        return FormatJson::encode( $newObject, true, FormatJson::UTF8_OK );
563    }
564
565    /**
566     * Validates the options
567     *
568     * @return array
569     */
570    private function getOptions() {
571        // Get and validate --wait
572        $wait = $this->getOption( 'wait' );
573        if ( $wait && !is_numeric( $wait ) ) {
574            $this->fatalError( 'The flag "--wait" should be used with a numeric value. E.g. "--wait 100"' );
575        }
576
577        // Get and validate --force and --merge flags
578        $force = $this->getOption( 'force' ) ?? false;
579        $merge = $this->getOption( 'merge' ) ?? false;
580        if ( $force && $merge ) {
581            $this->fatalError( 'The flags "--force" and "--merge" should be mutually exclusive:' . "\n"
582                . 'Use "--force" if you want to fully override the existing content with '
583                . 'the initial builtin version.' . "\n"
584                . 'Use "--merge if you want to re-insert the builtin versions but keep existing '
585                . 'multilingual data of every object.' );
586        }
587
588        // Get --current and --builtin only if --merge is set to true
589        $current = $this->getOption( 'current' ) ?? false;
590        $builtin = $this->getOption( 'builtin' ) ?? false;
591        if ( !$merge && ( $current || $builtin ) ) {
592            $this->fatalError( 'The flags "--current" or "--builtin" should only be used along with "--merge".' );
593        }
594        if ( $current && $builtin ) {
595            $this->fatalError( 'The flags "--current" and "--builtin" should be mutually exclusive:' . "\n"
596                . 'Use "--merge --builtin" to automatically default to restoring builtin versions.' . "\n"
597                . 'Use "--merge --current" to automatically default to keeping current stored versions.' );
598        }
599
600        // Get and validate Zid range to insert:
601        $all = $this->getOption( 'all' ) ?? false;
602        $from = $this->getOption( 'from' );
603        $to = $this->getOption( 'to' );
604        $zid = $this->getOption( 'zid' );
605
606        if ( $all ) {
607            // --all option overrides any --from and --to passed as arguments
608            if ( (bool)$from || (bool)$to || (bool)$zid ) {
609                $this->fatalError( 'The flag "--all" should not be used along with "--for", "--to" or "--zid".' );
610            }
611
612            $from = 1;
613            $to = 9999;
614        } elseif ( $zid ) {
615            // Remove Z or z
616            if ( strtoupper( substr( $zid, 0, 1 ) ) === 'Z' ) {
617                $zid = substr( $zid, 1 );
618            }
619
620            if ( !is_numeric( $zid ) || $zid < 1 || $zid > 9999 ) {
621                $this->fatalError( 'The flag "--zid" must be a number between 1 and 9999.' );
622            }
623
624            if ( (bool)$from || (bool)$to ) {
625                $this->fatalError( 'The flag "--zid" should not be used along with "--for", "--to" or "--all".' );
626            }
627
628            $from = $zid;
629            $to = $zid;
630        } else {
631            // If no --all and no --zid are entered, then --from and --to are mandatory
632            if ( (bool)$from xor (bool)$to ) {
633                $this->fatalError( 'The flag "--from" must be used with the flag "--to" to set a range.' );
634            }
635
636            // Remove Z or z
637            if ( strtoupper( substr( $from ?? '', 0, 1 ) ) === 'Z' ) {
638                $from = substr( $from, 1 );
639            }
640
641            // Remove Z or z
642            if ( strtoupper( substr( $to ?? '', 0, 1 ) ) === 'Z' ) {
643                $to = substr( $to, 1 );
644            }
645
646            if ( !is_numeric( $from ) || $from < 1 || $from > 9999 ) {
647                $this->fatalError( 'The flag "--from" must be followed by a Zid between Z1 and Z9999.' );
648            }
649
650            if ( !is_numeric( $to ) || $to < 1 || $to > 9999 ) {
651                $this->fatalError( 'The flag "--to" must be followed by a Zid between Z1 and Z9999.' );
652            }
653
654            if ( $from > $to ) {
655                $this->fatalError( 'The flag "--from" must be lower than the flag "--to".' );
656            }
657        }
658
659        return [
660            $all,
661            (int)$from,
662            (int)$to,
663            $force,
664            $merge,
665            $builtin,
666            $current,
667            (int)$wait
668        ];
669    }
670}
671
672$maintClass = LoadPreDefinedObject::class;
673require_once RUN_MAINTENANCE_IF_MAIN;