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