Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.19% |
170 / 253 |
|
29.41% |
5 / 17 |
CRAP | |
0.00% |
0 / 1 |
TitleLibrary | |
67.19% |
170 / 253 |
|
29.41% |
5 / 17 |
286.34 | |
0.00% |
0 / 1 |
register | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
2 | |||
checkNamespace | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
7.77 | |||
getInexpensiveTitleData | |
78.95% |
15 / 19 |
|
0.00% |
0 / 1 |
6.34 | |||
getExpensiveData | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
7.10 | |||
newTitle | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
9.08 | |||
makeTitle | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
getUrl | |
86.96% |
20 / 23 |
|
0.00% |
0 / 1 |
7.11 | |||
getContentInternal | |
47.83% |
11 / 23 |
|
0.00% |
0 / 1 |
13.96 | |||
getContent | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getCategories | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
getFileInfo | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
72 | |||
makeArrayOneBased | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
protectionLevels | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
3.05 | |||
cascadingProtection | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
3.01 | |||
redirectTarget | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
recordVaryFlag | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getPageLangCode | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Content\Content; |
7 | use MediaWiki\Logger\LoggerFactory; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Parser\ParserOutputFlags; |
10 | use MediaWiki\Revision\RevisionAccessException; |
11 | use MediaWiki\Revision\SlotRecord; |
12 | use MediaWiki\Title\Title; |
13 | |
14 | class TitleLibrary extends LibraryBase { |
15 | // Note these caches are naturally limited to |
16 | // $wgExpensiveParserFunctionLimit + 1 actual Title objects because any |
17 | // addition besides the one for the current page calls |
18 | // incrementExpensiveFunctionCount() |
19 | /** @var Title[] */ |
20 | private $titleCache = []; |
21 | /** @var (Title|null)[] */ |
22 | private $idCache = [ 0 => null ]; |
23 | |
24 | public function register() { |
25 | $lib = [ |
26 | 'newTitle' => [ $this, 'newTitle' ], |
27 | 'makeTitle' => [ $this, 'makeTitle' ], |
28 | 'getExpensiveData' => [ $this, 'getExpensiveData' ], |
29 | 'getUrl' => [ $this, 'getUrl' ], |
30 | 'getContent' => [ $this, 'getContent' ], |
31 | 'getCategories' => [ $this, 'getCategories' ], |
32 | 'getFileInfo' => [ $this, 'getFileInfo' ], |
33 | 'protectionLevels' => [ $this, 'protectionLevels' ], |
34 | 'cascadingProtection' => [ $this, 'cascadingProtection' ], |
35 | 'redirectTarget' => [ $this, 'redirectTarget' ], |
36 | 'recordVaryFlag' => [ $this, 'recordVaryFlag' ], |
37 | 'getPageLangCode' => [ $this, 'getPageLangCode' ], |
38 | ]; |
39 | $title = $this->getTitle(); |
40 | return $this->getEngine()->registerInterface( 'mw.title.lua', $lib, [ |
41 | 'thisTitle' => $title ? $this->getInexpensiveTitleData( $title ) : null, |
42 | 'NS_MEDIA' => NS_MEDIA, |
43 | ] ); |
44 | } |
45 | |
46 | /** |
47 | * Check a namespace parameter |
48 | * @param string $name Function name (for errors) |
49 | * @param int $argIdx Argument index (for errors) |
50 | * @param mixed &$arg Argument |
51 | * @param int|null $default Default value, if $arg is null |
52 | */ |
53 | private function checkNamespace( $name, $argIdx, &$arg, $default = null ) { |
54 | if ( $arg === null && $default !== null ) { |
55 | $arg = $default; |
56 | } elseif ( is_numeric( $arg ) ) { |
57 | $arg = (int)$arg; |
58 | if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $arg ) ) { |
59 | throw new LuaError( |
60 | "bad argument #$argIdx to '$name' (unrecognized namespace number '$arg')" |
61 | ); |
62 | } |
63 | } elseif ( is_string( $arg ) ) { |
64 | $ns = MediaWikiServices::getInstance()->getContentLanguage()->getNsIndex( $arg ); |
65 | if ( $ns === false ) { |
66 | throw new LuaError( |
67 | "bad argument #$argIdx to '$name' (unrecognized namespace name '$arg')" |
68 | ); |
69 | } |
70 | $arg = $ns; |
71 | } else { |
72 | $this->checkType( $name, $argIdx, $arg, 'namespace number or name' ); |
73 | } |
74 | } |
75 | |
76 | /** |
77 | * Extract inexpensive information from a Title object for return to Lua |
78 | * |
79 | * @param Title $title Title to return |
80 | * @return array Lua data |
81 | */ |
82 | private function getInexpensiveTitleData( Title $title ) { |
83 | $ns = $title->getNamespace(); |
84 | $ret = [ |
85 | 'isCurrentTitle' => (bool)$title->equals( $this->getTitle() ), |
86 | 'isLocal' => (bool)$title->isLocal(), |
87 | 'interwiki' => $title->getInterwiki(), |
88 | 'namespace' => $ns, |
89 | 'nsText' => $title->getNsText(), |
90 | 'text' => $title->getText(), |
91 | 'fragment' => $title->getFragment(), |
92 | 'thePartialUrl' => $title->getPartialURL(), |
93 | ]; |
94 | if ( $ns === NS_SPECIAL ) { |
95 | // Core doesn't currently record special page links, but it may in the future. |
96 | if ( $this->getParser() && !$title->equals( $this->getTitle() ) ) { |
97 | $this->getParser()->getOutput()->addLink( $title ); |
98 | } |
99 | $ret['exists'] = MediaWikiServices::getInstance() |
100 | ->getSpecialPageFactory()->exists( $title->getDBkey() ); |
101 | } |
102 | if ( $ns !== NS_FILE && $ns !== NS_MEDIA ) { |
103 | $ret['file'] = false; |
104 | } |
105 | return $ret; |
106 | } |
107 | |
108 | /** |
109 | * Extract expensive information from a Title object for return to Lua |
110 | * |
111 | * This records a link to this title in the current ParserOutput and caches the |
112 | * title for repeated lookups. It may call incrementExpensiveFunctionCount() if |
113 | * the title is not already cached. |
114 | * |
115 | * @internal |
116 | * @param string $text Title text |
117 | * @return array Lua data |
118 | */ |
119 | public function getExpensiveData( $text ) { |
120 | $this->checkType( 'getExpensiveData', 1, $text, 'string' ); |
121 | $title = Title::newFromText( $text ); |
122 | if ( !$title ) { |
123 | return [ null ]; |
124 | } |
125 | $dbKey = $title->getPrefixedDBkey(); |
126 | if ( isset( $this->titleCache[$dbKey] ) ) { |
127 | // It was already cached, so we already did the expensive work and added a link |
128 | $title = $this->titleCache[$dbKey]; |
129 | } else { |
130 | if ( !$title->equals( $this->getTitle() ) ) { |
131 | $this->incrementExpensiveFunctionCount(); |
132 | |
133 | // Record a link |
134 | if ( $this->getParser() ) { |
135 | $this->getParser()->getOutput()->addLink( $title ); |
136 | } |
137 | } |
138 | |
139 | // Cache it |
140 | $this->titleCache[$dbKey] = $title; |
141 | if ( $title->getArticleID() > 0 ) { |
142 | $this->idCache[$title->getArticleID()] = $title; |
143 | } |
144 | } |
145 | |
146 | $ret = [ |
147 | 'isRedirect' => (bool)$title->isRedirect(), |
148 | 'id' => $title->getArticleID(), |
149 | 'contentModel' => $title->getContentModel(), |
150 | ]; |
151 | if ( $title->getNamespace() === NS_SPECIAL ) { |
152 | $ret['exists'] = MediaWikiServices::getInstance() |
153 | ->getSpecialPageFactory()->exists( $title->getDBkey() ); |
154 | } else { |
155 | // bug 70495: don't just check whether the ID != 0 |
156 | $ret['exists'] = $title->exists(); |
157 | } |
158 | return [ $ret ]; |
159 | } |
160 | |
161 | /** |
162 | * Handler for title.new |
163 | * |
164 | * Calls Title::newFromID or Title::newFromTitle as appropriate for the |
165 | * arguments. |
166 | * |
167 | * @internal |
168 | * @param string|int $text_or_id Title or page_id to fetch |
169 | * @param string|int|null $defaultNamespace Namespace name or number to use if |
170 | * $text_or_id doesn't override |
171 | * @return array Lua data |
172 | */ |
173 | public function newTitle( $text_or_id, $defaultNamespace = null ) { |
174 | $type = $this->getLuaType( $text_or_id ); |
175 | if ( $type === 'number' ) { |
176 | if ( array_key_exists( $text_or_id, $this->idCache ) ) { |
177 | $title = $this->idCache[$text_or_id]; |
178 | } else { |
179 | $this->incrementExpensiveFunctionCount(); |
180 | $title = Title::newFromID( $text_or_id ); |
181 | $this->idCache[$text_or_id] = $title; |
182 | |
183 | // Record a link |
184 | if ( $title && $this->getParser() && !$title->equals( $this->getTitle() ) ) { |
185 | $this->getParser()->getOutput()->addLink( $title ); |
186 | } |
187 | } |
188 | if ( $title ) { |
189 | $this->titleCache[$title->getPrefixedDBkey()] = $title; |
190 | } else { |
191 | return [ null ]; |
192 | } |
193 | } elseif ( $type === 'string' ) { |
194 | $this->checkNamespace( 'title.new', 2, $defaultNamespace, NS_MAIN ); |
195 | |
196 | // Note this just fills in the given fields, it doesn't fetch from |
197 | // the page table. |
198 | $title = Title::newFromText( $text_or_id, $defaultNamespace ); |
199 | if ( !$title ) { |
200 | return [ null ]; |
201 | } |
202 | } else { |
203 | $this->checkType( 'title.new', 1, $text_or_id, 'number or string' ); |
204 | throw new LogicException( 'checkType above should have failed' ); |
205 | } |
206 | |
207 | return [ $this->getInexpensiveTitleData( $title ) ]; |
208 | } |
209 | |
210 | /** |
211 | * Handler for title.makeTitle |
212 | * |
213 | * Calls Title::makeTitleSafe. |
214 | * |
215 | * @internal |
216 | * @param string|int $ns Namespace |
217 | * @param string $text Title text |
218 | * @param string|null $fragment URI fragment |
219 | * @param string|null $interwiki Interwiki code |
220 | * @return array Lua data |
221 | */ |
222 | public function makeTitle( $ns, $text, $fragment = null, $interwiki = null ) { |
223 | $this->checkNamespace( 'makeTitle', 1, $ns ); |
224 | $this->checkType( 'makeTitle', 2, $text, 'string' ); |
225 | $this->checkTypeOptional( 'makeTitle', 3, $fragment, 'string', '' ); |
226 | $this->checkTypeOptional( 'makeTitle', 4, $interwiki, 'string', '' ); |
227 | |
228 | // Note this just fills in the given fields, it doesn't fetch from the |
229 | // page table. |
230 | $title = Title::makeTitleSafe( $ns, $text, $fragment, $interwiki ); |
231 | if ( !$title ) { |
232 | return [ null ]; |
233 | } |
234 | |
235 | return [ $this->getInexpensiveTitleData( $title ) ]; |
236 | } |
237 | |
238 | /** |
239 | * Get a URL referring to this title |
240 | * @internal |
241 | * @param string $text Title text. |
242 | * @param string $which 'fullUrl', 'localUrl', or 'canonicalUrl' |
243 | * @param string|array|null $query Query string or query string data. |
244 | * @param string|null $proto 'http', 'https', 'relative', or 'canonical' |
245 | * @return array |
246 | */ |
247 | public function getUrl( $text, $which, $query = null, $proto = null ) { |
248 | static $protoMap = [ |
249 | 'http' => PROTO_HTTP, |
250 | 'https' => PROTO_HTTPS, |
251 | 'relative' => PROTO_RELATIVE, |
252 | 'canonical' => PROTO_CANONICAL, |
253 | ]; |
254 | |
255 | $this->checkType( 'getUrl', 1, $text, 'string' ); |
256 | $this->checkType( 'getUrl', 2, $which, 'string' ); |
257 | if ( !in_array( $which, [ 'fullUrl', 'localUrl', 'canonicalUrl' ], true ) ) { |
258 | $this->checkType( 'getUrl', 2, $which, "'fullUrl', 'localUrl', or 'canonicalUrl'" ); |
259 | } |
260 | |
261 | // May call the following Title methods: |
262 | // getFullUrl, getLocalUrl, getCanonicalUrl |
263 | $func = "get" . ucfirst( $which ); |
264 | |
265 | $args = [ $query, false ]; |
266 | if ( !is_string( $query ) && !is_array( $query ) ) { |
267 | $this->checkTypeOptional( $which, 1, $query, 'table or string', '' ); |
268 | } |
269 | if ( $which === 'fullUrl' ) { |
270 | $this->checkTypeOptional( $which, 2, $proto, 'string', 'relative' ); |
271 | if ( !isset( $protoMap[$proto] ) ) { |
272 | $this->checkType( $which, 2, $proto, "'http', 'https', 'relative', or 'canonical'" ); |
273 | } |
274 | $args[] = $protoMap[$proto]; |
275 | } |
276 | |
277 | $title = Title::newFromText( $text ); |
278 | if ( !$title ) { |
279 | return [ null ]; |
280 | } |
281 | return [ $title->$func( ...$args ) ]; |
282 | } |
283 | |
284 | /** |
285 | * Utility to get a Content object from a title |
286 | * |
287 | * The title is counted as a transclusion. |
288 | * |
289 | * @param string $text Title text |
290 | * @return Content|null The Content object of the title, null if missing |
291 | */ |
292 | private function getContentInternal( $text ) { |
293 | $title = Title::newFromText( $text ); |
294 | if ( !$title || !$title->canExist() ) { |
295 | return null; |
296 | } |
297 | |
298 | $rev = $this->getParser()->fetchCurrentRevisionRecordOfTitle( $title ); |
299 | |
300 | if ( $title->equals( $this->getTitle() ) ) { |
301 | $parserOutput = $this->getParser()->getOutput(); |
302 | $parserOutput->setOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1 ); |
303 | $parserOutput->setRevisionUsedSha1Base36( $rev ? $rev->getSha1() : '' ); |
304 | wfDebug( __METHOD__ . ": set vary-revision-sha1 for '$title'" ); |
305 | } else { |
306 | // Record in templatelinks, so edits cause the page to be refreshed |
307 | $this->getParser()->getOutput()->addTemplate( |
308 | $title, $title->getArticleID(), $title->getLatestRevID() |
309 | ); |
310 | } |
311 | |
312 | if ( !$rev ) { |
313 | return null; |
314 | } |
315 | |
316 | try { |
317 | $content = $rev->getContent( SlotRecord::MAIN ); |
318 | } catch ( RevisionAccessException $ex ) { |
319 | $logger = LoggerFactory::getInstance( 'Scribunto' ); |
320 | $logger->warning( |
321 | __METHOD__ . ': Unable to transclude revision content', |
322 | [ 'exception' => $ex ] |
323 | ); |
324 | $content = null; |
325 | } |
326 | return $content; |
327 | } |
328 | |
329 | /** |
330 | * Handler for getContent |
331 | * @internal |
332 | * @param string $text |
333 | * @return string[]|null[] |
334 | */ |
335 | public function getContent( $text ) { |
336 | $this->checkType( 'getContent', 1, $text, 'string' ); |
337 | $content = $this->getContentInternal( $text ); |
338 | return [ $content ? $content->serialize() : null ]; |
339 | } |
340 | |
341 | /** |
342 | * @internal |
343 | * @param string $text |
344 | * @return string[][] |
345 | */ |
346 | public function getCategories( $text ) { |
347 | $this->checkType( 'getCategories', 1, $text, 'string' ); |
348 | $title = Title::newFromText( $text ); |
349 | if ( !$title ) { |
350 | return [ [] ]; |
351 | } |
352 | $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
353 | $this->incrementExpensiveFunctionCount(); |
354 | |
355 | $parserOutput = $this->getParser()->getOutput(); |
356 | if ( $title->equals( $this->getTitle() ) ) { |
357 | $parserOutput->setOutputFlag( ParserOutputFlags::VARY_REVISION ); |
358 | } else { |
359 | // Record in templatelinks, so edits cause the page to be refreshed |
360 | $parserOutput->addTemplate( $title, $title->getArticleID(), $title->getLatestRevID() ); |
361 | } |
362 | |
363 | $categoryTitles = $page->getCategories(); |
364 | $categoryNames = []; |
365 | foreach ( $categoryTitles as $title ) { |
366 | $categoryNames[] = $title->getText(); |
367 | } |
368 | return [ self::makeArrayOneBased( $categoryNames ) ]; |
369 | } |
370 | |
371 | /** |
372 | * Handler for getFileInfo |
373 | * @internal |
374 | * @param string $text |
375 | * @return array |
376 | */ |
377 | public function getFileInfo( $text ) { |
378 | $this->checkType( 'getFileInfo', 1, $text, 'string' ); |
379 | $title = Title::newFromText( $text ); |
380 | if ( !$title ) { |
381 | return [ false ]; |
382 | } |
383 | $ns = $title->getNamespace(); |
384 | if ( $ns !== NS_FILE && $ns !== NS_MEDIA ) { |
385 | return [ false ]; |
386 | } |
387 | |
388 | $this->incrementExpensiveFunctionCount(); |
389 | $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ); |
390 | if ( !$file ) { |
391 | return [ [ 'exists' => false ] ]; |
392 | } |
393 | $this->getParser()->getOutput()->addImage( |
394 | $file->getName(), $file->getTimestamp(), $file->getSha1() |
395 | ); |
396 | if ( !$file->exists() ) { |
397 | return [ [ 'exists' => false ] ]; |
398 | } |
399 | $pageCount = $file->pageCount(); |
400 | if ( $pageCount === false ) { |
401 | $pages = null; |
402 | } else { |
403 | $pages = []; |
404 | for ( $i = 1; $i <= $pageCount; ++$i ) { |
405 | $pages[$i] = [ |
406 | 'width' => $file->getWidth( $i ), |
407 | 'height' => $file->getHeight( $i ) |
408 | ]; |
409 | } |
410 | } |
411 | return [ [ |
412 | 'exists' => true, |
413 | 'width' => $file->getWidth(), |
414 | 'height' => $file->getHeight(), |
415 | 'mimeType' => $file->getMimeType(), |
416 | 'length' => $file->getLength(), |
417 | 'size' => $file->getSize(), |
418 | 'pages' => $pages |
419 | ] ]; |
420 | } |
421 | |
422 | /** |
423 | * Renumber an array for return to Lua |
424 | * @param array $arr |
425 | * @return array |
426 | */ |
427 | private static function makeArrayOneBased( $arr ) { |
428 | if ( !$arr ) { |
429 | return $arr; |
430 | } |
431 | return array_combine( range( 1, count( $arr ) ), array_values( $arr ) ); |
432 | } |
433 | |
434 | /** |
435 | * Handler for protectionLevels |
436 | * @internal |
437 | * @param string $text |
438 | * @return array |
439 | */ |
440 | public function protectionLevels( $text ) { |
441 | $this->checkType( 'protectionLevels', 1, $text, 'string' ); |
442 | $title = Title::newFromText( $text ); |
443 | if ( !$title ) { |
444 | return [ null ]; |
445 | } |
446 | |
447 | $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore(); |
448 | |
449 | if ( !$restrictionStore->areRestrictionsLoaded( $title ) ) { |
450 | $this->incrementExpensiveFunctionCount(); |
451 | } |
452 | return [ array_map( |
453 | [ self::class, 'makeArrayOneBased' ], |
454 | $restrictionStore->getAllRestrictions( $title ) |
455 | ) ]; |
456 | } |
457 | |
458 | /** |
459 | * Handler for cascadingProtection |
460 | * @internal |
461 | * @param string $text |
462 | * @return array |
463 | */ |
464 | public function cascadingProtection( $text ) { |
465 | $this->checkType( 'cascadingProtection', 1, $text, 'string' ); |
466 | $title = Title::newFromText( $text ); |
467 | if ( !$title ) { |
468 | return [ null ]; |
469 | } |
470 | |
471 | $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore(); |
472 | $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter(); |
473 | |
474 | if ( !$restrictionStore->areCascadeProtectionSourcesLoaded( $title ) ) { |
475 | $this->incrementExpensiveFunctionCount(); |
476 | } |
477 | |
478 | [ $sources, $restrictions ] = $restrictionStore->getCascadeProtectionSources( $title ); |
479 | |
480 | return [ [ |
481 | 'sources' => self::makeArrayOneBased( array_map( |
482 | static function ( $t ) use ( $titleFormatter ) { |
483 | return $titleFormatter->getPrefixedText( $t ); |
484 | }, |
485 | $sources ) ), |
486 | 'restrictions' => array_map( |
487 | [ self::class, 'makeArrayOneBased' ], |
488 | $restrictions |
489 | ) |
490 | ] ]; |
491 | } |
492 | |
493 | /** |
494 | * Handler for redirectTarget |
495 | * @internal |
496 | * @param string $text |
497 | * @return string[]|null[] |
498 | */ |
499 | public function redirectTarget( $text ) { |
500 | $this->checkType( 'redirectTarget', 1, $text, 'string' ); |
501 | $content = $this->getContentInternal( $text ); |
502 | $redirTitle = $content ? $content->getRedirectTarget() : null; |
503 | return [ $redirTitle ? $this->getInexpensiveTitleData( $redirTitle ) : null ]; |
504 | } |
505 | |
506 | /** |
507 | * Record a ParserOutput flag when the current title is accessed |
508 | * @internal |
509 | * @param string $text |
510 | * @param string $flag |
511 | * @return array |
512 | */ |
513 | public function recordVaryFlag( $text, $flag ) { |
514 | $this->checkType( 'recordVaryFlag', 1, $text, 'string' ); |
515 | $this->checkType( 'recordVaryFlag', 2, $flag, 'string' ); |
516 | $title = Title::newFromText( $text ); |
517 | if ( $title && $title->equals( $this->getTitle() ) ) { |
518 | // XXX note that we don't check this against the values defined |
519 | // in ParserOutputFlags |
520 | $this->getParser()->getOutput()->setOutputFlag( $flag ); |
521 | } |
522 | return []; |
523 | } |
524 | |
525 | /** |
526 | * Handler for getPageLangCode |
527 | * @internal |
528 | * @param string $text Title text. |
529 | * @return array<?string> |
530 | */ |
531 | public function getPageLangCode( $text ) { |
532 | $title = Title::newFromText( $text ); |
533 | if ( $title ) { |
534 | // If the page language is coming from the page record, we've |
535 | // probably accounted for the cost of reading the title from |
536 | // the DB already. However, a PageContentLanguage hook handler |
537 | // might get invoked here, and who knows how much that costs. |
538 | // Be safe and increment here, even though this could over-count. |
539 | $this->incrementExpensiveFunctionCount(); |
540 | return [ $title->getPageLanguage()->getCode() ]; |
541 | } |
542 | return [ null ]; |
543 | } |
544 | } |