Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.29% covered (warning)
64.29%
171 / 266
18.18% covered (danger)
18.18%
2 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
JCSingleton
64.29% covered (warning)
64.29%
171 / 266
18.18% covered (danger)
18.18%
2 / 11
672.27
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 parseConfiguration
79.53% covered (warning)
79.53%
101 / 127
0.00% covered (danger)
0.00%
0 / 1
75.20
 getConfVal
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getConfObject
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 getContentFromLocalCache
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getContent
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 parseContent
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getTitleMap
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getContentClass
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 parseTitle
84.75% covered (warning)
84.75%
50 / 59
0.00% covered (danger)
0.00%
0 / 1
28.40
 getMetadata
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2namespace JsonConfig;
3
4use GenderCache;
5use InvalidArgumentException;
6use MapCacheLRU;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\Linker\LinkTarget;
9use MediaWiki\MainConfigNames;
10use MediaWiki\MainConfigSchema;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Title\MalformedTitleException;
13use MediaWiki\Title\MediaWikiTitleCodec;
14use MediaWiki\Title\Title;
15use MediaWiki\Title\TitleParser;
16use MediaWiki\Title\TitleValue;
17use stdClass;
18
19/**
20 * Static utility methods and configuration page hook handlers for JsonConfig extension.
21 *
22 * @file
23 * @ingroup Extensions
24 * @ingroup JsonConfig
25 * @author Yuri Astrakhan
26 * @copyright © 2013 Yuri Astrakhan
27 * @license GPL-2.0-or-later
28 */
29class JCSingleton {
30    /**
31     * @var array describes how a title should be handled by JsonConfig extension.
32     * The structure is an array of array of ...:
33     * { int_namespace => { name => { allows-sub-namespaces => configuration_array } } }
34     */
35    public static $titleMap = [];
36
37    /**
38     * @var string[]|false[] containing all the namespaces handled by JsonConfig
39     * Maps namespace id (int) => namespace name (string).
40     * If false, presumes the namespace has been registered by core or another extension
41     */
42    public static $namespaces = [];
43
44    /**
45     * @var MapCacheLRU[] contains a cache of recently resolved JCTitle's
46     *   as namespace => MapCacheLRU
47     */
48    public static $titleMapCacheLru = [];
49
50    /**
51     * @var MapCacheLRU[] contains a cache of recently requested content objects
52     *   as namespace => MapCacheLRU
53     */
54    public static $mapCacheLru = [];
55
56    /**
57     * @var TitleParser cached invariant title parser
58     */
59    public static $titleParser;
60
61    /**
62     * Initializes singleton state by parsing $wgJsonConfig* values
63     * @param bool $force Force init, only usable in unit tests
64     */
65    public static function init( $force = false ) {
66        static $isInitialized = false;
67        if ( $isInitialized && !$force ) {
68            return;
69        }
70        if ( $force && !defined( 'MW_PHPUNIT_TEST' ) ) {
71            throw new \LogicException( 'Can force init only in tests' );
72        }
73        $isInitialized = true;
74        $config = MediaWikiServices::getInstance()->getMainConfig();
75        [ self::$titleMap, self::$namespaces ] = self::parseConfiguration(
76            $config->get( MainConfigNames::NamespaceContentModels ),
77            $config->get( MainConfigNames::ContentHandlers ),
78            array_replace_recursive(
79                \ExtensionRegistry::getInstance()->getAttribute( 'JsonConfigs' ), $config->get( 'JsonConfigs' )
80            ),
81            array_replace_recursive(
82                \ExtensionRegistry::getInstance()->getAttribute( 'JsonConfigModels' ),
83                $config->get( 'JsonConfigModels' )
84            )
85        );
86    }
87
88    /**
89     * @param array $namespaceContentModels $wgNamespaceContentModels
90     * @param array $contentHandlers $wgContentHandlers
91     * @param array $configs $wgJsonConfigs
92     * @param array $models $wgJsonConfigModels
93     * @param bool $warn if true, calls wfLogWarning() for all errors
94     * @return array [ $titleMap, $namespaces ]
95     */
96    public static function parseConfiguration(
97        array $namespaceContentModels, array $contentHandlers,
98        array $configs, array $models, $warn = true
99    ) {
100        $defaultModelId = 'JsonConfig';
101        $warnFunc = $warn
102            ? 'wfLogWarning'
103            : static function ( $msg ) {
104            };
105
106        $namespaces = [];
107        $titleMap = [];
108        foreach ( $configs as $confId => &$conf ) {
109            if ( !is_string( $confId ) ) {
110                $warnFunc(
111                    "JsonConfig: Invalid \$wgJsonConfigs['$confId'], the key must be a string"
112                );
113                continue;
114            }
115            if ( self::getConfObject( $warnFunc, $conf, $confId ) === null ) {
116                continue; // warned inside the function
117            }
118
119            $modelId = property_exists( $conf, 'model' )
120                ? ( $conf->model ? : $defaultModelId ) : $confId;
121            if ( !array_key_exists( $modelId, $models ) ) {
122                if ( $modelId === $defaultModelId ) {
123                    $models[$defaultModelId] = null;
124                } else {
125                    $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs['$confId']: " .
126                        "Model '$modelId' is not defined in \$wgJsonConfigModels" );
127                    continue;
128                }
129            }
130            if ( array_key_exists( $modelId, $contentHandlers ) ) {
131                $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs['$confId']: Model '$modelId' is " .
132                    "already registered in \$contentHandlers to {$contentHandlers[$modelId]}" );
133                continue;
134            }
135            $conf->model = $modelId;
136
137            $ns = self::getConfVal( $conf, 'namespace', NS_CONFIG );
138            if ( !is_int( $ns ) || $ns % 2 !== 0 ) {
139                $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs['$confId']: " .
140                    "Namespace $ns should be an even number" );
141                continue;
142            }
143            // Even though we might be able to override default content model for namespace,
144            // lets keep things clean
145            if ( array_key_exists( $ns, $namespaceContentModels ) ) {
146                $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs['$confId']: Namespace $ns is " .
147                    "already set to handle model '$namespaceContentModels[$ns]'" );
148                continue;
149            }
150
151            // nsName & nsTalk are handled later
152            self::getConfVal( $conf, 'pattern', '' );
153            self::getConfVal( $conf, 'cacheExp', 24 * 60 * 60 );
154            self::getConfVal( $conf, 'cacheKey', '' );
155            self::getConfVal( $conf, 'flaggedRevs', false );
156            self::getConfVal( $conf, 'license', false );
157            $islocal = self::getConfVal( $conf, 'isLocal', true );
158
159            // Decide if matching configs should be stored on this wiki
160            $storeHere = $islocal || property_exists( $conf, 'store' );
161            if ( !$storeHere ) {
162                // 'store' does not exist, use it as a flag to indicate remote storage
163                $conf->store = false;
164                $remote = self::getConfObject( $warnFunc, $conf, 'remote', $confId, 'url' );
165                if ( $remote === null ) {
166                    continue; // warned inside the function
167                }
168                if ( self::getConfVal( $remote, 'url', '' ) === '' ) {
169                    $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs['$confId']['remote']['url']: " .
170                        "API URL is not set, and this config is not being stored locally" );
171                    continue;
172                }
173                self::getConfVal( $remote, 'username', '' );
174                self::getConfVal( $remote, 'password', '' );
175            } else {
176                if ( property_exists( $conf, 'remote' ) ) {
177                    // non-fatal -- simply ignore the 'remote' setting
178                    $warnFunc( "JsonConfig: In \$wgJsonConfigs['$confId']['remote'] is set for " .
179                        "the config that will be stored on this wiki. " .
180                        "'remote' parameter will be ignored."
181                    );
182                }
183                $conf->remote = null;
184                $store = self::getConfObject( $warnFunc, $conf, 'store', $confId );
185                if ( $store === null ) {
186                    continue; // warned inside the function
187                }
188                self::getConfVal( $store, 'cacheNewValue', true );
189                self::getConfVal( $store, 'notifyUrl', '' );
190                self::getConfVal( $store, 'notifyUsername', '' );
191                self::getConfVal( $store, 'notifyPassword', '' );
192            }
193
194            // Too lazy to write proper error messages for all parameters.
195            if ( ( isset( $conf->nsTalk ) && !is_string( $conf->nsTalk ) ) ||
196                !is_string( $conf->pattern ) ||
197                !is_bool( $islocal ) || !is_int( $conf->cacheExp ) || !is_string( $conf->cacheKey )
198                || !is_bool( $conf->flaggedRevs )
199            ) {
200                $warnFunc( "JsonConfig: Invalid type of one of the parameters in " .
201                    "\$wgJsonConfigs['$confId'], please check documentation" );
202                continue;
203            }
204            if ( isset( $remote ) ) {
205                if ( !is_string( $remote->url ) || !is_string( $remote->username ) ||
206                    !is_string( $remote->password )
207                ) {
208                    $warnFunc( "JsonConfig: Invalid type of one of the parameters in " .
209                        "\$wgJsonConfigs['$confId']['remote'], please check documentation" );
210                    continue;
211                }
212            }
213            if ( isset( $store ) ) {
214                if ( !is_bool( $store->cacheNewValue ) || !is_string( $store->notifyUrl ) ||
215                    !is_string( $store->notifyUsername ) || !is_string( $store->notifyPassword )
216                ) {
217                    $warnFunc( "JsonConfig: Invalid type of one of the parameters in " .
218                        " \$wgJsonConfigs['$confId']['store'], please check documentation" );
219                    continue;
220                }
221            }
222            if ( $storeHere ) {
223                // If nsName is given, add it to the list, together with the talk page
224                // Otherwise, create a placeholder for it
225                if ( property_exists( $conf, 'nsName' ) ) {
226                    if ( $conf->nsName === false ) {
227                        // Non JC-specific namespace, don't register it
228                        if ( !array_key_exists( $ns, $namespaces ) ) {
229                            $namespaces[$ns] = false;
230                        }
231                    } elseif ( $ns === NS_CONFIG ) {
232                        $warnFunc( "JsonConfig: Parameter 'nsName' in \$wgJsonConfigs['$confId'] " .
233                            "is not supported for namespace == NS_CONFIG ($ns)" );
234                    } else {
235                        $nsName = $conf->nsName;
236                        $nsTalk = $conf->nsTalk ?? $nsName . '_talk';
237                        if ( !is_string( $nsName ) || $nsName === '' ) {
238                            $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs['$confId']: " .
239                                    "if given, nsName must be a string" );
240                            continue;
241                        } elseif ( array_key_exists( $ns, $namespaces ) &&
242                                $namespaces[$ns] !== null
243                        ) {
244                            if ( $namespaces[$ns] !== $nsName ||
245                                $namespaces[$ns + 1] !== $nsTalk
246                            ) {
247                                $warnFunc( "JsonConfig: \$wgJsonConfigs['$confId'] - " .
248                                        "nsName has already been set for namespace $ns" );
249                            }
250                        } else {
251                            $namespaces[$ns] = $nsName;
252                            $namespaces[$ns + 1] = $conf->nsTalk ?? $nsName . '_talk';
253                        }
254                    }
255                } elseif ( !array_key_exists( $ns, $namespaces ) || $namespaces[$ns] === false ) {
256                    $namespaces[$ns] = null;
257                }
258            }
259
260            if ( !array_key_exists( $ns, $titleMap ) ) {
261                $titleMap[$ns] = [ $conf ];
262            } else {
263                $titleMap[$ns][] = $conf;
264            }
265        }
266
267        // Add all undeclared namespaces
268        $missingNs = 1;
269        foreach ( $namespaces as $ns => $nsName ) {
270            if ( $nsName === null ) {
271                $nsName = 'Config';
272                if ( $ns !== NS_CONFIG ) {
273                    $nsName .= $missingNs;
274                    $warnFunc(
275                        "JsonConfig: Namespace $ns does not have 'nsName' defined, using '$nsName'"
276                    );
277                    $missingNs += 1;
278                }
279                $namespaces[$ns] = $nsName;
280                $namespaces[$ns + 1] = $nsName . '_talk';
281            }
282        }
283
284        return [ $titleMap, $namespaces ];
285    }
286
287    /**
288     * Helper function to check if configuration has a field set, and if not, set it to default
289     * @param stdClass &$conf
290     * @param string $field
291     * @param mixed $default
292     * @return mixed
293     */
294    private static function getConfVal( &$conf, $field, $default ) {
295        if ( property_exists( $conf, $field ) ) {
296            return $conf->$field;
297        }
298        $conf->$field = $default;
299        return $default;
300    }
301
302    /**
303     * Helper function to check if configuration has a field set, and if not, set it to default
304     * @param callable $warnFunc
305     * @param stdClass &$value
306     * @param string $field
307     * @param string|null $confId
308     * @param string|null $treatAsField
309     * @return null|stdClass
310     */
311    private static function getConfObject(
312        $warnFunc, &$value, $field, $confId = null, $treatAsField = null
313    ) {
314        if ( !$confId ) {
315            $val = & $value;
316        } else {
317            if ( !property_exists( $value, $field ) ) {
318                $value->$field = null;
319            }
320            $val = & $value->$field;
321        }
322        if ( $val === null || $val === true ) {
323            $val = (object)[];
324        } elseif ( is_array( $val ) ) {
325            $val = (object)$val;
326        } elseif ( is_string( $val ) && $treatAsField !== null ) {
327            // treating this string value as a sub-field
328            $val = (object)[ $treatAsField => $val ];
329        } elseif ( !is_object( $val ) ) {
330            $warnFunc( "JsonConfig: Invalid \$wgJsonConfigs" . ( $confId ? "['$confId']" : "" ) .
331                "['$field'], the value must be either an array or an object" );
332            return null;
333        }
334        return $val;
335    }
336
337    /**
338     * Get content object from the local LRU cache, or null if doesn't exist
339     * @param TitleValue $titleValue
340     * @return null|JCContent
341     */
342    public static function getContentFromLocalCache( TitleValue $titleValue ) {
343        // Some of the titleValues are remote, and their namespace might not be declared
344        // in the current wiki. Since TitleValue is a content object, it does not validate
345        // the existence of namespace, hence we use it as a simple storage.
346        // Producing an artificial string key by appending (namespaceID . ':' . titleDbKey)
347        // seems wasteful and redundant, plus most of the time there will be just a single
348        // namespace declared, so this structure seems efficient and easy enough.
349        if ( !array_key_exists( $titleValue->getNamespace(), self::$mapCacheLru ) ) {
350            // TBD: should cache size be a config value?
351            self::$mapCacheLru[$titleValue->getNamespace()] = $cache = new MapCacheLRU( 10 );
352        } else {
353            $cache = self::$mapCacheLru[$titleValue->getNamespace()];
354        }
355
356        return $cache->get( $titleValue->getDBkey() );
357    }
358
359    /**
360     * Get content object for the given title.
361     * Namespace ID does not need to be defined in the current wiki,
362     * as long as it is defined in $wgJsonConfigs.
363     * @param TitleValue|JCTitle $titleValue
364     * @return bool|JCContent Returns false if the title is not handled by the settings
365     */
366    public static function getContent( TitleValue $titleValue ) {
367        $content = self::getContentFromLocalCache( $titleValue );
368
369        if ( $content === null ) {
370            $jct = self::parseTitle( $titleValue );
371            if ( $jct ) {
372                $store = new JCCache( $jct );
373                $content = $store->get();
374                if ( is_string( $content ) ) {
375                    // Convert string to the content object if needed
376                    $handler = new JCContentHandler( $jct->getConfig()->model );
377                    $content = $handler->unserializeContent( $content, null, false );
378                }
379            } else {
380                $content = false;
381            }
382            self::$mapCacheLru[$titleValue->getNamespace()]
383                ->set( $titleValue->getDBkey(), $content );
384        }
385
386        return $content;
387    }
388
389    /**
390     * Parse json text into a content object for the given title.
391     * Namespace ID does not need to be defined in the current wiki,
392     * as long as it is defined in $wgJsonConfigs.
393     * @param TitleValue $titleValue
394     * @param string $jsonText json content
395     * @param bool $isSaving if true, performs extensive validation during unserialization
396     * @return bool|JCContent Returns false if the title is not handled by the settings
397     */
398    public static function parseContent( TitleValue $titleValue, $jsonText, $isSaving = false ) {
399        $jct = self::parseTitle( $titleValue );
400        if ( $jct ) {
401            $handler = new JCContentHandler( $jct->getConfig()->model );
402            return $handler->unserializeContent( $jsonText, null, $isSaving );
403        }
404
405        return false;
406    }
407
408    /**
409     * Mostly for debugging purposes, this function returns initialized internal JsonConfig settings
410     * @return array[] map of namespaceIDs to list of configurations
411     */
412    public static function getTitleMap() {
413        self::init();
414        return self::$titleMap;
415    }
416
417    /**
418     * Get the name of the class for a given content model
419     * @param string $modelId
420     * @return string
421     * @phan-return class-string
422     */
423    public static function getContentClass( $modelId ) {
424        $configModels = array_replace_recursive(
425            \ExtensionRegistry::getInstance()->getAttribute( 'JsonConfigModels' ),
426            MediaWikiServices::getInstance()->getMainConfig()->get( 'JsonConfigModels' )
427        );
428        $class = null;
429        if ( array_key_exists( $modelId, $configModels ) ) {
430            $value = $configModels[$modelId];
431            if ( is_array( $value ) ) {
432                if ( !array_key_exists( 'class', $value ) ) {
433                    wfLogWarning( "JsonConfig: Invalid \$wgJsonConfigModels['$modelId'] array " .
434                        "value, 'class' not found" );
435                } else {
436                    $class = $value['class'];
437                }
438            } else {
439                $class = $value;
440            }
441        }
442        if ( !$class ) {
443            $class = JCContent::class;
444        }
445        return $class;
446    }
447
448    /**
449     * Given a title (either a user-given string, or as an object), return JCTitle
450     * @param Title|TitleValue|string $value
451     * @param int|null $namespace Only used when title is a string
452     * @return JCTitle|null|false false if unrecognized namespace,
453     * and null if namespace is handled but does not match this title
454     */
455    public static function parseTitle( $value, $namespace = null ) {
456        if ( $value === null || $value === '' || $value === false ) {
457            // In some weird cases $value is null
458            return false;
459        } elseif ( $value instanceof JCTitle ) {
460            // Nothing to do
461            return $value;
462        } elseif ( $namespace !== null && !is_int( $namespace ) ) {
463            throw new InvalidArgumentException( '$namespace parameter must be either null or an integer' );
464        }
465
466        // figure out the namespace ID (int) - we don't need to parse the string if ns is unknown
467        if ( $value instanceof LinkTarget ) {
468            if ( $namespace === null ) {
469                $namespace = $value->getNamespace();
470            }
471        } elseif ( is_string( $value ) ) {
472            if ( $namespace === null ) {
473                throw new InvalidArgumentException( '$namespace parameter is missing for string $value' );
474            }
475        } else {
476            wfLogWarning( 'Unexpected title param type ' . gettype( $value ) );
477            return false;
478        }
479
480        // Search title map for the matching configuration
481        $map = self::getTitleMap();
482        if ( array_key_exists( $namespace, $map ) ) {
483            // Get appropriate LRU cache object
484            if ( !array_key_exists( $namespace, self::$titleMapCacheLru ) ) {
485                self::$titleMapCacheLru[$namespace] = $cache = new MapCacheLRU( 20 );
486            } else {
487                $cache = self::$titleMapCacheLru[$namespace];
488            }
489
490            // Parse string if needed
491            // TODO: should the string parsing also be cached?
492            if ( is_string( $value ) ) {
493                $language = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' );
494                if ( !self::$titleParser ) {
495                    // XXX Direct instantiation of MediaWikiTitleCodec isn't allowed. If core
496                    // doesn't support our use-case, core needs to be fixed to allow this.
497                    $oldArgStyle =
498                        ( new \ReflectionMethod( MediaWikiTitleCodec::class, '__construct' ) )
499                        ->getParameters()[2]->getName() === 'localInterwikis';
500                    self::$titleParser = new MediaWikiTitleCodec(
501                        $language,
502                        new GenderCache(),
503                        $oldArgStyle ? []
504                            // @phan-suppress-next-line PhanUndeclaredConstantOfClass Not merged yet
505                            : new ServiceOptions( MediaWikiTitleCodec::CONSTRUCTOR_OPTIONS, [
506                                MainConfigNames::LegalTitleChars =>
507                                    MainConfigSchema::LegalTitleChars['default'],
508                                MainConfigNames::LocalInterwikis => [],
509                            ] ),
510                        new FauxInterwikiLookup(),
511                        MediaWikiServices::getInstance()->getNamespaceInfo()
512                    );
513                }
514                // Interwiki prefixes are a special case for title parsing:
515                // first letter is not capitalized, namespaces are not resolved, etc.
516                // So we prepend an interwiki prefix to fool title codec, and later remove it.
517                try {
518                    $value = FauxInterwikiLookup::INTERWIKI_PREFIX . ':' . $value;
519                    $title = self::$titleParser->parseTitle( $value );
520
521                    // Defensive coding - ensure the parsing has proceeded as expected
522                    if ( $title->getDBkey() === '' || $title->getNamespace() !== NS_MAIN ||
523                        $title->hasFragment() ||
524                        $title->getInterwiki() !== FauxInterwikiLookup::INTERWIKI_PREFIX
525                    ) {
526                        return null;
527                    }
528                } catch ( MalformedTitleException $e ) {
529                    return null;
530                }
531
532                // At this point, only support wiki namespaces that capitalize title's first char,
533                // but do not enable sub-pages.
534                // This way data can already be stored on MediaWiki namespace everywhere, or
535                // places like commons and zerowiki.
536                // Another implicit limitation: there might be an issue if data is stored on a wiki
537                // with the non-default ucfirst(), e.g. az, kaa, kk, tr -- they convert "i" to "İ"
538                $dbKey = $language->ucfirst( $title->getDBkey() );
539            } else {
540                $dbKey = $value->getDBkey();
541            }
542
543            // A bit weird here: cache will store JCTitle objects or false if the namespace
544            // is known to JsonConfig but the dbkey does not match. But in case the title is not
545            // handled, this function returns null instead of false if the namespace is known,
546            // and false otherwise
547            $result = $cache->get( $dbKey );
548            if ( $result === null ) {
549                $result = false;
550                foreach ( $map[$namespace] as $conf ) {
551                    $re = $conf->pattern;
552                    if ( !$re || preg_match( $re, $dbKey ) ) {
553                        $result = new JCTitle( $namespace, $dbKey, $conf );
554                        break;
555                    }
556                }
557
558                $cache->set( $dbKey, $result );
559            }
560
561            // return null if the given namespace is mentioned in the config,
562            // but title doesn't match
563            return $result ?: null;
564
565        } else {
566            // return false if JC doesn't know anything about this namespace
567            return false;
568        }
569    }
570
571    /**
572     * Returns an array with settings if the $titleValue object is handled by the JsonConfig
573     * extension, false if unrecognized namespace,
574     * and null if namespace is handled but not this title
575     * @param TitleValue $titleValue
576     * @return stdClass|false|null
577     * @deprecated use JCSingleton::parseTitle() instead
578     */
579    public static function getMetadata( $titleValue ) {
580        $jct = self::parseTitle( $titleValue );
581        return $jct ? $jct->getConfig() : $jct;
582    }
583
584}