MediaWiki REL1_37
LinkHolderArray.php
Go to the documentation of this file.
1<?php
27
35 public $internals = [];
37 public $interwikis = [];
39 public $size = 0;
40
44 public $parent;
45
51
55 private $hookRunner;
56
63 HookContainer $hookContainer = null
64 ) {
65 $this->parent = $parent;
66
67 if ( !$languageConverter ) {
68 wfDeprecated( __METHOD__ . ' without $languageConverter parameter', '1.35' );
69 $languageConverter = MediaWikiServices::getInstance()
70 ->getLanguageConverterFactory()
71 ->getLanguageConverter( $parent->getTargetLanguage() );
72 }
73 $this->languageConverter = $languageConverter;
74 if ( !$hookContainer ) {
75 wfDeprecated( __METHOD__ . ' without $hookContainer parameter', '1.35' );
76 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
77 }
78 $this->hookRunner = new HookRunner( $hookContainer );
79 }
80
84 public function __destruct() {
85 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
86 foreach ( $this as $name => $_ ) {
87 unset( $this->$name );
88 }
89 }
90
95 public function merge( $other ) {
96 foreach ( $other->internals as $ns => $entries ) {
97 $this->size += count( $entries );
98 if ( !isset( $this->internals[$ns] ) ) {
99 $this->internals[$ns] = $entries;
100 } else {
101 $this->internals[$ns] += $entries;
102 }
103 }
104 $this->interwikis += $other->interwikis;
105 }
106
111 public function isBig() {
113 return $this->size > $wgLinkHolderBatchSize;
114 }
115
120 public function clear() {
121 $this->internals = [];
122 $this->interwikis = [];
123 $this->size = 0;
124 }
125
138 public function makeHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
139 # Separate the link trail from the rest of the link
140 list( $inside, $trail ) = Linker::splitTrail( $trail );
141
142 $key = $this->parent->nextLinkID();
143 $entry = [
144 'title' => $nt,
145 'text' => $prefix . $text . $inside,
146 'pdbk' => $nt->getPrefixedDBkey(),
147 ];
148
149 $this->size++;
150 if ( $nt->isExternal() ) {
151 // Use a globally unique ID to keep the objects mergable
152 $this->interwikis[$key] = $entry;
153 return "<!--IWLINK'\" $key-->{$trail}";
154 } else {
155 $ns = $nt->getNamespace();
156 $this->internals[$ns][$key] = $entry;
157 return "<!--LINK'\" $ns:$key-->{$trail}";
158 }
159 }
160
166 public function replace( &$text ) {
167 $this->replaceInternal( $text );
168 $this->replaceInterwiki( $text );
169 }
170
175 protected function replaceInternal( &$text ) {
176 if ( !$this->internals ) {
177 return;
178 }
179
180 $classes = [];
181 $services = MediaWikiServices::getInstance();
182 $linkCache = $services->getLinkCache();
183 $output = $this->parent->getOutput();
184 $linkRenderer = $this->parent->getLinkRenderer();
185
187
188 # Sort by namespace
189 ksort( $this->internals );
190
191 $pagemap = [];
192
193 # Generate query
194 $linkBatchFactory = $services->getLinkBatchFactory();
195 $lb = $linkBatchFactory->newLinkBatch();
196 $lb->setCaller( __METHOD__ );
197
198 foreach ( $this->internals as $ns => $entries ) {
199 foreach ( $entries as [ 'title' => $title, 'pdbk' => $pdbk ] ) {
202 # Skip invalid entries.
203 # Result will be ugly, but prevents crash.
204 if ( $title === null ) {
205 continue;
206 }
207
208 # Check if it's a static known link, e.g. interwiki
209 if ( $title->isAlwaysKnown() ) {
210 $classes[$pdbk] = '';
211 } elseif ( $ns == NS_SPECIAL ) {
212 $classes[$pdbk] = 'new';
213 } else {
214 $id = $linkCache->getGoodLinkID( $pdbk );
215 if ( $id != 0 ) {
216 $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
217 $output->addLink( $title, $id );
218 $pagemap[$id] = $pdbk;
219 } elseif ( $linkCache->isBadLink( $pdbk ) ) {
220 $classes[$pdbk] = 'new';
221 } else {
222 # Not in the link cache, add it to the query
223 $lb->addObj( $title );
224 }
225 }
226 }
227 }
228 if ( !$lb->isEmpty() ) {
229 $fields = array_merge(
230 LinkCache::getSelectFields(),
231 [ 'page_namespace', 'page_title' ]
232 );
233
234 $res = $dbr->select(
235 'page',
236 $fields,
237 $lb->constructSet( 'page', $dbr ),
238 __METHOD__
239 );
240
241 # Fetch data and form into an associative array
242 # non-existent = broken
243 foreach ( $res as $s ) {
244 $title = Title::makeTitle( $s->page_namespace, $s->page_title );
245 $pdbk = $title->getPrefixedDBkey();
246 $linkCache->addGoodLinkObjFromRow( $title, $s );
247 $output->addLink( $title, $s->page_id );
248 $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
249 // add id to the extension todolist
250 $pagemap[$s->page_id] = $pdbk;
251 }
252 unset( $res );
253 }
254 if ( $pagemap !== [] ) {
255 // pass an array of page_ids to an extension
256 $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
257 }
258
259 # Do a second query for different language variants of links and categories
260 if ( $this->languageConverter->hasVariants() ) {
261 $this->doVariants( $classes );
262 }
263
264 # Construct search and replace arrays
265 $replacePairs = [];
266 foreach ( $this->internals as $ns => $entries ) {
267 foreach ( $entries as $index => $entry ) {
268 $pdbk = $entry['pdbk'];
269 $title = $entry['title'];
270 $query = $entry['query'] ?? [];
271 $searchkey = "<!--LINK'\" $ns:$index-->";
272 $displayTextHtml = $entry['text'];
273 if ( isset( $entry['selflink'] ) ) {
274 $replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayTextHtml, $query );
275 continue;
276 }
277 if ( $displayTextHtml === '' ) {
278 $displayText = null;
279 } else {
280 $displayText = new HtmlArmor( $displayTextHtml );
281 }
282 if ( !isset( $classes[$pdbk] ) ) {
283 $classes[$pdbk] = 'new';
284 }
285 if ( $classes[$pdbk] == 'new' ) {
286 $linkCache->addBadLinkObj( $title );
287 $output->addLink( $title, 0 );
288 $link = $linkRenderer->makeBrokenLink(
289 $title, $displayText, [], $query
290 );
291 } else {
292 $link = $linkRenderer->makePreloadedLink(
293 $title, $displayText, $classes[$pdbk], [], $query
294 );
295 }
296
297 $replacePairs[$searchkey] = $link;
298 }
299 }
300
301 # Do the thing
302 $text = preg_replace_callback(
303 '/(<!--LINK\'" .*?-->)/',
304 static function ( array $matches ) use ( $replacePairs ) {
305 return $replacePairs[$matches[1]];
306 },
307 $text
308 );
309 }
310
315 protected function replaceInterwiki( &$text ) {
316 if ( empty( $this->interwikis ) ) {
317 return;
318 }
319
320 # Make interwiki link HTML
321 $output = $this->parent->getOutput();
322 $replacePairs = [];
323 $linkRenderer = $this->parent->getLinkRenderer();
324 foreach ( $this->interwikis as $key => $link ) {
325 $replacePairs[$key] = $linkRenderer->makeLink(
326 $link['title'],
327 new HtmlArmor( $link['text'] )
328 );
329 $output->addInterwikiLink( $link['title'] );
330 }
331
332 $text = preg_replace_callback(
333 '/<!--IWLINK\'" (.*?)-->/',
334 static function ( array $matches ) use ( $replacePairs ) {
335 return $replacePairs[$matches[1]];
336 },
337 $text
338 );
339 }
340
345 protected function doVariants( &$classes ) {
346 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
347 $linkBatch = $linkBatchFactory->newLinkBatch();
348 $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
349 $output = $this->parent->getOutput();
350 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
351 $titlesToBeConverted = '';
352 $titlesAttrs = [];
353
354 // Concatenate titles to a single string, thus we only need auto convert the
355 // single string to all variants. This would improve parser's performance
356 // significantly.
357 foreach ( $this->internals as $ns => $entries ) {
358 if ( $ns == NS_SPECIAL ) {
359 continue;
360 }
361 foreach ( $entries as $index => [ 'title' => $title, 'pdbk' => $pdbk ] ) {
362 // we only deal with new links (in its first query)
363 if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
364 $titlesAttrs[] = [ $index, $title ];
365 // separate titles with \0 because it would never appears
366 // in a valid title
367 $titlesToBeConverted .= $title->getText() . "\0";
368 }
369 }
370 }
371
372 // Now do the conversion and explode string to text of titles
373 $titlesAllVariants = $this->languageConverter->
374 autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
375 foreach ( $titlesAllVariants as &$titlesVariant ) {
376 $titlesVariant = explode( "\0", $titlesVariant );
377 }
378
379 // Then add variants of links to link batch
380 $parentTitle = $this->parent->getTitle();
381 foreach ( $titlesAttrs as $i => [ $index, $title ] ) {
383 $ns = $title->getNamespace();
384 $text = $title->getText();
385
386 foreach ( $titlesAllVariants as $variantName => $textVariants ) {
387 $textVariant = $textVariants[$i];
388 if ( $textVariant === $text ) {
389 continue;
390 }
391
392 $variantTitle = Title::makeTitle( $ns, $textVariant );
393
394 // Self-link checking for mixed/different variant titles. At this point, we
395 // already know the exact title does not exist, so the link cannot be to a
396 // variant of the current title that exists as a separate page.
397 if ( $variantTitle->equals( $parentTitle ) && !$title->hasFragment() ) {
398 $this->internals[$ns][$index]['selflink'] = true;
399 continue 2;
400 }
401
402 $linkBatch->addObj( $variantTitle );
403 $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
404 }
405 }
406
407 // process categories, check if a category exists in some variant
408 $categoryMap = []; // maps $category_variant => $category (dbkeys)
409 $varCategories = []; // category replacements oldDBkey => newDBkey
410 foreach ( $output->getCategoryLinks() as $category ) {
411 $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
412 $linkBatch->addObj( $categoryTitle );
413 $variants = $this->languageConverter->autoConvertToAllVariants( $category );
414 foreach ( $variants as $variant ) {
415 if ( $variant !== $category ) {
416 $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
417 if ( $variantTitle === null ) {
418 continue;
419 }
420 $linkBatch->addObj( $variantTitle );
421 $categoryMap[$variant] = [ $category, $categoryTitle ];
422 }
423 }
424 }
425
426 if ( !$linkBatch->isEmpty() ) {
427 // construct query
429 $fields = array_merge(
430 LinkCache::getSelectFields(),
431 [ 'page_namespace', 'page_title' ]
432 );
433
434 $varRes = $dbr->select( 'page',
435 $fields,
436 $linkBatch->constructSet( 'page', $dbr ),
437 __METHOD__
438 );
439
440 $pagemap = [];
441 $linkRenderer = $this->parent->getLinkRenderer();
442
443 // for each found variants, figure out link holders and replace
444 foreach ( $varRes as $s ) {
445 $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
446 $varPdbk = $variantTitle->getPrefixedDBkey();
447 $vardbk = $variantTitle->getDBkey();
448
449 $holderKeys = [];
450 if ( isset( $variantMap[$varPdbk] ) ) {
451 $holderKeys = $variantMap[$varPdbk];
452 $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
453 $output->addLink( $variantTitle, $s->page_id );
454 }
455
456 // loop over link holders
457 foreach ( $holderKeys as $key ) {
458 list( $ns, $index ) = explode( ':', $key, 2 );
459 $entry =& $this->internals[$ns][$index];
460 $pdbk = $entry['pdbk'];
461
462 if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
463 // found link in some of the variants, replace the link holder data
464 $entry['title'] = $variantTitle;
465 $entry['pdbk'] = $varPdbk;
466
467 // set pdbk and colour
468 $classes[$varPdbk] = $linkRenderer->getLinkClasses( $variantTitle );
469 $pagemap[$s->page_id] = $pdbk;
470 }
471 }
472
473 // check if the object is a variant of a category
474 if ( isset( $categoryMap[$vardbk] ) ) {
475 list( $oldkey, $oldtitle ) = $categoryMap[$vardbk];
476 if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
477 $varCategories[$oldkey] = $vardbk;
478 }
479 }
480 }
481 $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
482
483 // rebuild the categories in original order (if there are replacements)
484 if ( $varCategories !== [] ) {
485 $newCats = [];
486 $originalCats = $output->getCategories();
487 foreach ( $originalCats as $cat => $sortkey ) {
488 // make the replacement
489 $newCats[$varCategories[$cat] ?? $cat] = $sortkey;
490 }
491 $output->setCategoryLinks( $newCats );
492 }
493 }
494 }
495
503 public function replaceText( $text ) {
504 return preg_replace_callback(
505 '/<!--(IW)?LINK\'" (.*?)-->/',
506 function ( $matches ) {
507 list( $unchanged, $isInterwiki, $key ) = $matches;
508
509 if ( !$isInterwiki ) {
510 list( $ns, $index ) = explode( ':', $key, 2 );
511 return $this->internals[$ns][$index]['text'] ?? $unchanged;
512 } else {
513 return $this->interwikis[$key]['text'] ?? $unchanged;
514 }
515 },
516 $text
517 );
518 }
519}
$wgLinkHolderBatchSize
LinkHolderArray batch size For debugging.
const NS_SPECIAL
Definition Defines.php:53
const NS_CATEGORY
Definition Defines.php:78
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
doVariants(&$classes)
Modify $this->internals and $classes according to language variant linking rules.
replaceInterwiki(&$text)
Replace interwiki links.
merge( $other)
Merge another LinkHolderArray into this one.
replaceText( $text)
Replace and link placeholders with plain text of links (not HTML-formatted).
ILanguageConverter $languageConverter
Current language converter.
replaceInternal(&$text)
Replace internal links.
__destruct()
Reduce memory usage to reduce the impact of circular references.
HookRunner $hookRunner
clear()
Clear all stored link holders.
isBig()
Returns true if the memory requirements of this object are getting large.
__construct(Parser $parent, ILanguageConverter $languageConverter=null, HookContainer $hookContainer=null)
makeHolder(Title $nt, $text='', $trail='', $prefix='')
Make a link placeholder.
replace(&$text)
Replace link placeholders with actual links, in the buffer.
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:161
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition Linker.php:1993
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
MediaWikiServices is the service locator for the application scope of MediaWiki.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:91
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1156
Represents a title within MediaWiki.
Definition Title.php:48
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1078
getPrefixedDBkey()
Get the prefixed database key form.
Definition Title.php:1909
isExternal()
Is this Title interwiki?
Definition Title.php:968
The shared interface for all language converters.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
const DB_REPLICA
Definition defines.php:25