Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.05% |
155 / 267 |
|
16.67% |
2 / 12 |
CRAP | |
0.00% |
0 / 1 |
JCSingleton | |
58.05% |
155 / 267 |
|
16.67% |
2 / 12 |
1037.88 | |
0.00% |
0 / 1 |
init | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
parseConfiguration | |
79.53% |
101 / 127 |
|
0.00% |
0 / 1 |
75.20 | |||
getConfVal | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getConfObject | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
10 | |||
getContentFromLocalCache | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getContent | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
parseContent | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getTitleMap | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getContentClass | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
parseTitle | |
58.62% |
34 / 58 |
|
0.00% |
0 / 1 |
69.28 | |||
getMetadata | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
recordJsonLink | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | namespace JsonConfig; |
3 | |
4 | use InvalidArgumentException; |
5 | use MapCacheLRU; |
6 | use MediaWiki\Cache\GenderCache; |
7 | use MediaWiki\Config\ServiceOptions; |
8 | use MediaWiki\Linker\LinkTarget; |
9 | use MediaWiki\MainConfigNames; |
10 | use MediaWiki\MainConfigSchema; |
11 | use MediaWiki\MediaWikiServices; |
12 | use MediaWiki\Parser\ParserOutput; |
13 | use MediaWiki\Registration\ExtensionRegistry; |
14 | use MediaWiki\Title\MalformedTitleException; |
15 | use MediaWiki\Title\MediaWikiTitleCodec; |
16 | use MediaWiki\Title\Title; |
17 | use MediaWiki\Title\TitleParser; |
18 | use MediaWiki\Title\TitleValue; |
19 | use 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 | */ |
31 | class 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 | } |