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