Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 169 |
|
0.00% |
0 / 37 |
CRAP | |
0.00% |
0 / 1 |
StubMetadataCollector | |
0.00% |
0 / 169 |
|
0.00% |
0 / 37 |
5112 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addCategory | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
addWarningMsg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExternalLink | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getExternalLinks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setOutputFlag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
appendOutputStrings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
setUnsortedPageProperty | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setNumericPageProperty | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setJsConfigVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
appendExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
appendJsConfigVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addModuleStyles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setLimitReportData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTOCData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addLink | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
addImage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
addLanguageLink | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
addTemplate | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getLinkList | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
156 | |||
collect | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
110 | |||
get | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getModuleStyles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getJsConfigVars | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getCategoryNames | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getCategorySortKey | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getPageProperty | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOutputFlags | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getTOCData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setIndicator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIndicators | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
linkToString | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
stringToLink | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace Wikimedia\Parsoid\Config; |
6 | |
7 | use Psr\Log\LoggerInterface; |
8 | use Psr\Log\LogLevel; |
9 | use Wikimedia\Assert\UnreachableException; |
10 | use Wikimedia\Parsoid\Core\ContentMetadataCollector; |
11 | use Wikimedia\Parsoid\Core\ContentMetadataCollectorCompat; |
12 | use Wikimedia\Parsoid\Core\ContentMetadataCollectorStringSets as CMCSS; |
13 | use Wikimedia\Parsoid\Core\LinkTarget; |
14 | use Wikimedia\Parsoid\Core\TOCData; |
15 | use Wikimedia\Parsoid\Utils\TitleValue; |
16 | |
17 | /** |
18 | * Minimal implementation of a ContentMetadataCollector which just |
19 | * records all metadata in an array. Used for testing or operation |
20 | * in API mode. |
21 | */ |
22 | class StubMetadataCollector implements ContentMetadataCollector { |
23 | use ContentMetadataCollectorCompat; |
24 | |
25 | public const LINKTYPE_CATEGORY = 'category'; |
26 | public const LINKTYPE_LANGUAGE = 'language'; |
27 | public const LINKTYPE_INTERWIKI = 'interwiki'; |
28 | public const LINKTYPE_LOCAL = 'local'; |
29 | public const LINKTYPE_MEDIA = 'media'; |
30 | public const LINKTYPE_SPECIAL = 'special'; |
31 | public const LINKTYPE_TEMPLATE = 'template'; |
32 | |
33 | /** @var SiteConfig */ |
34 | private $siteConfig; |
35 | |
36 | /** @var LoggerInterface */ |
37 | private $logger; |
38 | |
39 | /** @var array<string,array> */ |
40 | private $mWarningMsgs = []; |
41 | |
42 | /** @var array */ |
43 | private $storage = []; |
44 | |
45 | /** @var string */ |
46 | private const MERGE_STRATEGY_KEY = '_parsoid-strategy_'; |
47 | |
48 | /** |
49 | * Non-standard merge strategy to use for properties which are *not* |
50 | * accumulators: "write-once" means that the property should be set |
51 | * once (although subsequently resetting it to the same value is ok) |
52 | * and an error will be thrown if there is an attempt to combine |
53 | * multiple values. |
54 | * |
55 | * This strategy is internal to the StubMetadataCollector for now; |
56 | * ParserOutput implements similar semantics for many of its properties, |
57 | * but not (yet) in a principled or uniform way. |
58 | */ |
59 | private const MERGE_STRATEGY_WRITE_ONCE = 'write-once'; |
60 | |
61 | /** |
62 | * @param SiteConfig $siteConfig Used to resolve title namespaces |
63 | * and to log warnings for unsafe metadata updates |
64 | */ |
65 | public function __construct( |
66 | SiteConfig $siteConfig |
67 | ) { |
68 | $this->siteConfig = $siteConfig; |
69 | $this->logger = $siteConfig->getLogger(); |
70 | } |
71 | |
72 | /** @inheritDoc */ |
73 | public function addCategory( $c, $sort = '' ): void { |
74 | // Numeric strings often become an `int` when passed to addCategory() |
75 | $this->collect( |
76 | self::LINKTYPE_CATEGORY, |
77 | $this->linkToString( $c ), |
78 | $sort, |
79 | self::MERGE_STRATEGY_WRITE_ONCE |
80 | ); |
81 | } |
82 | |
83 | /** @inheritDoc */ |
84 | public function addWarningMsg( string $msg, ...$args ): void { |
85 | $this->mWarningMsgs[$msg] = $args; |
86 | } |
87 | |
88 | /** @inheritDoc */ |
89 | public function addExternalLink( string $url ): void { |
90 | $this->collect( |
91 | 'externallinks', |
92 | $url, |
93 | '', |
94 | self::MERGE_STRATEGY_WRITE_ONCE |
95 | ); |
96 | } |
97 | |
98 | public function getExternalLinks(): array { |
99 | return array_keys( $this->get( 'externallinks' ) ); |
100 | } |
101 | |
102 | /** @inheritDoc */ |
103 | public function setOutputFlag( string $name, bool $value = true ): void { |
104 | $this->collect( 'outputflags', $name, (string)$value, self::MERGE_STRATEGY_WRITE_ONCE ); |
105 | } |
106 | |
107 | /** @inheritDoc */ |
108 | public function appendOutputStrings( string $name, array $value ): void { |
109 | foreach ( $value as $v ) { |
110 | $this->collect( 'outputstrings', $name, $v ); |
111 | } |
112 | } |
113 | |
114 | /** @inheritDoc */ |
115 | public function setUnsortedPageProperty( string $propName, string $value = '' ): void { |
116 | $this->collect( 'properties', $propName, $value, self::MERGE_STRATEGY_WRITE_ONCE ); |
117 | } |
118 | |
119 | /** @inheritDoc */ |
120 | public function setNumericPageProperty( string $propName, $numericValue ): void { |
121 | if ( !is_numeric( $numericValue ) ) { |
122 | throw new \TypeError( __METHOD__ . " with non-numeric value" ); |
123 | } |
124 | $value = 0 + $numericValue; # cast to number |
125 | $this->collect( 'properties', $propName, $value, self::MERGE_STRATEGY_WRITE_ONCE ); |
126 | } |
127 | |
128 | /** @inheritDoc */ |
129 | public function setExtensionData( string $key, $value ): void { |
130 | $this->collect( 'extensiondata', $key, $value, self::MERGE_STRATEGY_WRITE_ONCE ); |
131 | } |
132 | |
133 | /** @inheritDoc */ |
134 | public function setJsConfigVar( string $key, $value ): void { |
135 | $this->collect( 'jsconfigvars', $key, $value, self::MERGE_STRATEGY_WRITE_ONCE ); |
136 | } |
137 | |
138 | /** @inheritDoc */ |
139 | public function appendExtensionData( |
140 | string $key, |
141 | $value, |
142 | string $strategy = self::MERGE_STRATEGY_UNION |
143 | ): void { |
144 | $this->collect( 'extensiondata', $key, $value, $strategy ); |
145 | } |
146 | |
147 | /** @inheritDoc */ |
148 | public function appendJsConfigVar( |
149 | string $key, |
150 | string $value, |
151 | string $strategy = self::MERGE_STRATEGY_UNION |
152 | ): void { |
153 | $this->collect( 'jsconfigvars', $key, $value, $strategy ); |
154 | } |
155 | |
156 | /** @inheritDoc */ |
157 | public function addModules( array $modules ): void { |
158 | $this->appendOutputStrings( CMCSS::MODULE, $modules ); |
159 | } |
160 | |
161 | /** @inheritDoc */ |
162 | public function addModuleStyles( array $moduleStyles ): void { |
163 | $this->appendOutputStrings( CMCSS::MODULE_STYLE, $moduleStyles ); |
164 | } |
165 | |
166 | /** @inheritDoc */ |
167 | public function setLimitReportData( string $key, $value ): void { |
168 | // XXX maybe need to JSON-encode $value |
169 | $this->collect( 'limitreportdata', $key, $value, self::MERGE_STRATEGY_WRITE_ONCE ); |
170 | } |
171 | |
172 | /** @inheritDoc */ |
173 | public function setTOCData( TOCData $tocData ): void { |
174 | $this->collect( 'tocdata', '', $tocData, self::MERGE_STRATEGY_WRITE_ONCE ); |
175 | } |
176 | |
177 | /** @inheritDoc */ |
178 | public function addLink( LinkTarget $link, $id = null ): void { |
179 | # Fragments are stripped when collecting. |
180 | $link = $link->createFragmentTarget( '' ); |
181 | $type = self::LINKTYPE_LOCAL; |
182 | |
183 | if ( $link->isExternal() ) { |
184 | $type = self::LINKTYPE_INTERWIKI; |
185 | } elseif ( $link->inNamespace( -1 ) ) { |
186 | $type = self::LINKTYPE_SPECIAL; |
187 | } |
188 | |
189 | if ( $type === self::LINKTYPE_LOCAL && $link->getDbkey() === '' ) { |
190 | // Don't record self links - [[#Foo]] |
191 | return; |
192 | } |
193 | $this->collect( |
194 | $type, |
195 | $this->linkToString( $link ), |
196 | '', |
197 | self::MERGE_STRATEGY_WRITE_ONCE |
198 | ); |
199 | } |
200 | |
201 | /** @inheritDoc */ |
202 | public function addImage( LinkTarget $link, $timestamp = null, $sha1 = null ): void { |
203 | # Fragments are stripped when collecting. |
204 | $link = $link->createFragmentTarget( '' ); |
205 | $this->collect( |
206 | self::LINKTYPE_MEDIA, |
207 | $this->linkToString( $link ), |
208 | '', |
209 | self::MERGE_STRATEGY_WRITE_ONCE |
210 | ); |
211 | } |
212 | |
213 | /** @inheritDoc */ |
214 | public function addLanguageLink( LinkTarget $lt ): void { |
215 | # Fragments are *not* stripped from language links. |
216 | # Language links are deduplicated by the interwiki prefix |
217 | |
218 | # Note that, unlike some other types of collected metadata, |
219 | # language links are 'first wins' and the subsequent entries |
220 | # for the same language are ignored. |
221 | if ( $this->get( self::LINKTYPE_LANGUAGE, $lt->getInterwiki(), self::MERGE_STRATEGY_WRITE_ONCE ) !== null ) { |
222 | return; |
223 | } |
224 | |
225 | $this->collect( |
226 | self::LINKTYPE_LANGUAGE, |
227 | $lt->getInterwiki(), |
228 | $this->linkToString( $lt ), |
229 | self::MERGE_STRATEGY_WRITE_ONCE |
230 | ); |
231 | } |
232 | |
233 | /** |
234 | * Add a dependency on the given template. |
235 | * @param LinkTarget $link |
236 | * @param int $page_id |
237 | * @param int $rev_id |
238 | */ |
239 | public function addTemplate( LinkTarget $link, int $page_id, int $rev_id ): void { |
240 | # Fragments are stripped when collecting. |
241 | $link = $link->createFragmentTarget( '' ); |
242 | // XXX should store the page_id and rev_id |
243 | $this->collect( |
244 | self::LINKTYPE_TEMPLATE, |
245 | $this->linkToString( $link ), |
246 | '', |
247 | self::MERGE_STRATEGY_WRITE_ONCE |
248 | ); |
249 | } |
250 | |
251 | /** |
252 | * @see ParserOutput::getLinkList() |
253 | * @param string $linkType A link type, which should be a constant from |
254 | * this class |
255 | * @return list<array{link:LinkTarget,pageid?:int,revid?:int,sort?:string,time?:string|false,sha1?:string|false}> |
256 | */ |
257 | public function getLinkList( string $linkType ): array { |
258 | $result = []; |
259 | switch ( $linkType ) { |
260 | case self::LINKTYPE_CATEGORY: |
261 | foreach ( $this->get( $linkType ) as $link => $sort ) { |
262 | $result[] = [ |
263 | 'link' => $this->stringToLink( (string)$link ), |
264 | 'sort' => $sort, |
265 | ]; |
266 | } |
267 | break; |
268 | case self::LINKTYPE_LANGUAGE: |
269 | foreach ( $this->get( $linkType ) as $lang => $link ) { |
270 | $result[] = [ |
271 | 'link' => $this->stringToLink( $link ), |
272 | ]; |
273 | } |
274 | break; |
275 | case self::LINKTYPE_INTERWIKI: |
276 | case self::LINKTYPE_LOCAL: |
277 | case self::LINKTYPE_MEDIA: |
278 | case self::LINKTYPE_SPECIAL: |
279 | case self::LINKTYPE_TEMPLATE: |
280 | foreach ( $this->get( $linkType ) as $link => $ignore ) { |
281 | $result[] = [ |
282 | 'link' => $this->stringToLink( (string)$link ), |
283 | ]; |
284 | } |
285 | break; |
286 | default: |
287 | throw new UnreachableException( "Bad link type: $linkType" ); |
288 | } |
289 | return $result; |
290 | } |
291 | |
292 | /** |
293 | * Unified internal implementation of metadata collection. |
294 | * @param string $which Internal string identifying the type of metadata. |
295 | * @param string $key Key for storage (or '' if this is not relevant) |
296 | * @param mixed $value Value to store |
297 | * @param string $strategy "union" or "write-once" |
298 | */ |
299 | private function collect( |
300 | string $which, string $key, $value, |
301 | string $strategy = self::MERGE_STRATEGY_UNION |
302 | ): void { |
303 | if ( !array_key_exists( $which, $this->storage ) ) { |
304 | $this->storage[$which] = []; |
305 | } |
306 | if ( !array_key_exists( $key, $this->storage[$which] ) ) { |
307 | $this->storage[$which][$key] = [ self::MERGE_STRATEGY_KEY => $strategy ]; |
308 | if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) { |
309 | $this->storage[$which][$key]['value'] = $value; |
310 | return; |
311 | } |
312 | } |
313 | if ( $this->storage[$which][$key][self::MERGE_STRATEGY_KEY] !== $strategy ) { |
314 | $this->logger->log( |
315 | LogLevel::WARNING, |
316 | "Conflicting strategies for $which $key" |
317 | ); |
318 | // Destructive update for compatibility; this is deprecated! |
319 | unset( $this->storage[$which][$key] ); |
320 | $this->collect( $which, $key, $value, $strategy ); |
321 | return; |
322 | } |
323 | if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) { |
324 | if ( ( $this->storage[$which][$key]['value'] ?? null ) === $value ) { |
325 | return; // already exists with the desired value |
326 | } |
327 | $this->logger->log( |
328 | LogLevel::WARNING, |
329 | "Multiple writes to a write-once: $which $key" |
330 | ); |
331 | // Destructive update for compatibility; this is deprecated! |
332 | unset( $this->storage[$which][$key] ); |
333 | $this->collect( $which, $key, $value, $strategy ); |
334 | return; |
335 | } elseif ( $strategy === self::MERGE_STRATEGY_UNION ) { |
336 | if ( !( is_string( $value ) || is_int( $value ) ) ) { |
337 | throw new \InvalidArgumentException( "Bad value type for $key: " . get_debug_type( $value ) ); |
338 | } |
339 | $this->storage[$which][$key][$value] = true; |
340 | return; |
341 | } else { |
342 | throw new \InvalidArgumentException( "Unknown strategy: $strategy" ); |
343 | } |
344 | } |
345 | |
346 | /** |
347 | * Retrieve values from the collector. |
348 | * @param string $which Internal string identifying the type of metadata. |
349 | * @param string|null $key Key for storage (or '' if this is not relevant) |
350 | * @param string $defaultStrategy Determines whether to return an empty |
351 | * array or null for a missing $key |
352 | * @return mixed |
353 | */ |
354 | private function get( string $which, ?string $key = null, string $defaultStrategy = self::MERGE_STRATEGY_UNION ) { |
355 | if ( $key !== null ) { |
356 | $result = ( $this->storage[$which] ?? [] )[$key] ?? []; |
357 | $strategy = $result[self::MERGE_STRATEGY_KEY] ?? $defaultStrategy; |
358 | unset( $result[self::MERGE_STRATEGY_KEY] ); |
359 | if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) { |
360 | return $result['value'] ?? null; |
361 | } else { |
362 | return array_keys( $result ); |
363 | } |
364 | } |
365 | $result = []; |
366 | foreach ( ( $this->storage[$which] ?? [] ) as $key => $ignore ) { |
367 | $result[$key] = $this->get( $which, (string)$key ); |
368 | } |
369 | return $result; |
370 | } |
371 | |
372 | // @internal introspection methods |
373 | |
374 | /** @return string[] */ |
375 | public function getModules(): array { |
376 | return $this->get( 'outputstrings', CMCSS::MODULE ); |
377 | } |
378 | |
379 | /** @return string[] */ |
380 | public function getModuleStyles(): array { |
381 | return $this->get( 'outputstrings', CMCSS::MODULE_STYLE ); |
382 | } |
383 | |
384 | /** @return string[] */ |
385 | public function getJsConfigVars(): array { |
386 | // This is somewhat unusual, in that we expose the 'set' represenation |
387 | // as $key => true, instead of just returning array_keys(). |
388 | $result = $this->storage['jsconfigvars'] ?? []; |
389 | foreach ( $result as $key => &$value ) { |
390 | $strategy = $value[self::MERGE_STRATEGY_KEY] ?? null; |
391 | unset( $value[self::MERGE_STRATEGY_KEY] ); |
392 | if ( $strategy === self::MERGE_STRATEGY_WRITE_ONCE ) { |
393 | $value = array_keys( $value )[0]; |
394 | } |
395 | } |
396 | return $result; |
397 | } |
398 | |
399 | /** @return list<string> */ |
400 | public function getCategoryNames(): array { |
401 | return array_map( |
402 | fn ( $item ) => $item['link']->getDBkey(), |
403 | $this->getLinkList( self::LINKTYPE_CATEGORY ) |
404 | ); |
405 | } |
406 | |
407 | /** |
408 | * @param string $name Category name |
409 | * @return ?string Sort key |
410 | */ |
411 | public function getCategorySortKey( string $name ): ?string { |
412 | $tv = TitleValue::tryNew( |
413 | 14, // NS_CATEGORY |
414 | $name |
415 | ); |
416 | return $this->get( |
417 | self::LINKTYPE_CATEGORY, |
418 | $this->linkToString( $tv ), |
419 | self::MERGE_STRATEGY_WRITE_ONCE |
420 | ); |
421 | } |
422 | |
423 | /** |
424 | * @param string $name |
425 | * @return ?string |
426 | */ |
427 | public function getPageProperty( string $name ): ?string { |
428 | return $this->get( 'properties', $name, self::MERGE_STRATEGY_WRITE_ONCE ); |
429 | } |
430 | |
431 | /** |
432 | * Return the collected extension data under the given key. |
433 | * @param string $key |
434 | * @return mixed|null |
435 | */ |
436 | public function getExtensionData( string $key ) { |
437 | return $this->get( 'extensiondata', $key, self::MERGE_STRATEGY_WRITE_ONCE ); |
438 | } |
439 | |
440 | /** |
441 | * Return the active output flags. |
442 | * @return string[] |
443 | */ |
444 | public function getOutputFlags() { |
445 | $result = []; |
446 | foreach ( $this->get( 'outputflags', null ) as $key => $value ) { |
447 | if ( $value ) { |
448 | $result[] = $key; |
449 | } |
450 | } |
451 | return $result; |
452 | } |
453 | |
454 | /** |
455 | * Return the collected TOC data, or null if no TOC data was collected. |
456 | * @return ?TOCData |
457 | */ |
458 | public function getTOCData(): ?TOCData { |
459 | return $this->get( 'tocdata', '', self::MERGE_STRATEGY_WRITE_ONCE ); |
460 | } |
461 | |
462 | /** |
463 | * Set the content for an indicator. |
464 | * @param string $name |
465 | * @param string $content |
466 | */ |
467 | public function setIndicator( $name, $content ): void { |
468 | $this->collect( 'indicators', $name, $content, self::MERGE_STRATEGY_WRITE_ONCE ); |
469 | } |
470 | |
471 | /** |
472 | * Return a "name" => "content-id" mapping of recorded indicators |
473 | * @return array |
474 | */ |
475 | public function getIndicators(): array { |
476 | return $this->get( 'indicators' ); |
477 | } |
478 | |
479 | // helper functions for recording LinkTarget objects |
480 | |
481 | /** |
482 | * Convert a LinkTarget to a string for storing in the collected metadata. |
483 | * @param LinkTarget $lt |
484 | * @return string |
485 | */ |
486 | private function linkToString( LinkTarget $lt ): string { |
487 | return implode( '#', [ |
488 | (string)$lt->getNamespace(), |
489 | $lt->getDBkey(), |
490 | $lt->getInterwiki(), |
491 | $lt->getFragment(), |
492 | ] ); |
493 | } |
494 | |
495 | /** |
496 | * Convert a string back into a LinkTarget for retrieval from the |
497 | * collected metadata. |
498 | * @param string $s |
499 | * @return LinkTarget |
500 | */ |
501 | private function stringToLink( string $s ): LinkTarget { |
502 | [ $namespace, $dbkey, $interwiki, $fragment ] = explode( '#', $s, 4 ); |
503 | return TitleValue::tryNew( (int)$namespace, $dbkey, $fragment, $interwiki ); |
504 | } |
505 | } |