Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
64.29% |
171 / 266 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
JCSingleton | |
64.29% |
171 / 266 |
|
18.18% |
2 / 11 |
672.27 | |
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 | |
84.75% |
50 / 59 |
|
0.00% |
0 / 1 |
28.40 | |||
getMetadata | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | namespace JsonConfig; |
3 | |
4 | use GenderCache; |
5 | use InvalidArgumentException; |
6 | use MapCacheLRU; |
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\Title\MalformedTitleException; |
13 | use MediaWiki\Title\MediaWikiTitleCodec; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\Title\TitleParser; |
16 | use MediaWiki\Title\TitleValue; |
17 | use 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 | */ |
29 | class 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 | } |