MediaWiki REL1_39
LinkHolderArray.php
Go to the documentation of this file.
1<?php
28
36 public $internals = [];
38 public $interwikis = [];
40 public $size = 0;
41
45 public $parent;
46
51 private $languageConverter;
52
56 private $hookRunner;
57
63 public function __construct( Parser $parent, ILanguageConverter $languageConverter,
64 HookContainer $hookContainer
65 ) {
66 $this->parent = $parent;
67 $this->languageConverter = $languageConverter;
68 $this->hookRunner = new HookRunner( $hookContainer );
69 }
70
74 public function __destruct() {
75 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
76 foreach ( $this as $name => $_ ) {
77 unset( $this->$name );
78 }
79 }
80
85 public function merge( $other ) {
86 foreach ( $other->internals as $ns => $entries ) {
87 $this->size += count( $entries );
88 if ( !isset( $this->internals[$ns] ) ) {
89 $this->internals[$ns] = $entries;
90 } else {
91 $this->internals[$ns] += $entries;
92 }
93 }
94 $this->interwikis += $other->interwikis;
95 }
96
101 public function isBig() {
102 $linkHolderBatchSize = MediaWikiServices::getInstance()->getMainConfig()
103 ->get( MainConfigNames::LinkHolderBatchSize );
104 return $this->size > $linkHolderBatchSize;
105 }
106
111 public function clear() {
112 $this->internals = [];
113 $this->interwikis = [];
114 $this->size = 0;
115 }
116
129 public function makeHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
130 # Separate the link trail from the rest of the link
131 list( $inside, $trail ) = Linker::splitTrail( $trail );
132
133 $key = $this->parent->nextLinkID();
134 $entry = [
135 'title' => $nt,
136 'text' => $prefix . $text . $inside,
137 'pdbk' => $nt->getPrefixedDBkey(),
138 ];
139
140 $this->size++;
141 if ( $nt->isExternal() ) {
142 // Use a globally unique ID to keep the objects mergable
143 $this->interwikis[$key] = $entry;
144 return "<!--IWLINK'\" $key-->{$trail}";
145 } else {
146 $ns = $nt->getNamespace();
147 $this->internals[$ns][$key] = $entry;
148 return "<!--LINK'\" $ns:$key-->{$trail}";
149 }
150 }
151
157 public function replace( &$text ) {
158 $this->replaceInternal( $text );
159 $this->replaceInterwiki( $text );
160 }
161
166 protected function replaceInternal( &$text ) {
167 if ( !$this->internals ) {
168 return;
169 }
170
171 $classes = [];
172 $services = MediaWikiServices::getInstance();
173 $linkCache = $services->getLinkCache();
174 $output = $this->parent->getOutput();
175 $linkRenderer = $this->parent->getLinkRenderer();
176
178
179 # Sort by namespace
180 ksort( $this->internals );
181
182 $pagemap = [];
183
184 # Generate query
185 $linkBatchFactory = $services->getLinkBatchFactory();
186 $lb = $linkBatchFactory->newLinkBatch();
187 $lb->setCaller( __METHOD__ );
188
189 foreach ( $this->internals as $ns => $entries ) {
190 foreach ( $entries as [ 'title' => $title, 'pdbk' => $pdbk ] ) {
193 # Skip invalid entries.
194 # Result will be ugly, but prevents crash.
195 if ( $title === null ) {
196 continue;
197 }
198
199 # Check if it's a static known link, e.g. interwiki
200 if ( $title->isAlwaysKnown() ) {
201 $classes[$pdbk] = '';
202 } elseif ( $ns == NS_SPECIAL ) {
203 $classes[$pdbk] = 'new';
204 } else {
205 $id = $linkCache->getGoodLinkID( $pdbk );
206 if ( $id != 0 ) {
207 $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
208 $output->addLink( $title, $id );
209 $pagemap[$id] = $pdbk;
210 } elseif ( $linkCache->isBadLink( $pdbk ) ) {
211 $classes[$pdbk] = 'new';
212 } else {
213 # Not in the link cache, add it to the query
214 $lb->addObj( $title );
215 }
216 }
217 }
218 }
219 if ( !$lb->isEmpty() ) {
220 $res = $dbr->newSelectQueryBuilder()
221 ->select( LinkCache::getSelectFields() )
222 ->from( 'page' )
223 ->where( [ $lb->constructSet( 'page', $dbr ) ] )
224 ->caller( __METHOD__ )
225 ->fetchResultSet();
226
227 # Fetch data and form into an associative array
228 # non-existent = broken
229 foreach ( $res as $s ) {
230 $title = Title::makeTitle( $s->page_namespace, $s->page_title );
231 $pdbk = $title->getPrefixedDBkey();
232 $linkCache->addGoodLinkObjFromRow( $title, $s );
233 $output->addLink( $title, $s->page_id );
234 $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
235 // add id to the extension todolist
236 $pagemap[$s->page_id] = $pdbk;
237 }
238 unset( $res );
239 }
240 if ( $pagemap !== [] ) {
241 // pass an array of page_ids to an extension
242 $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
243 }
244
245 # Do a second query for different language variants of links and categories
246 if ( $this->languageConverter->hasVariants() ) {
247 $this->doVariants( $classes );
248 }
249
250 # Construct search and replace arrays
251 $replacePairs = [];
252 foreach ( $this->internals as $ns => $entries ) {
253 foreach ( $entries as $index => $entry ) {
254 $pdbk = $entry['pdbk'];
255 $title = $entry['title'];
256 $query = $entry['query'] ?? [];
257 $searchkey = "<!--LINK'\" $ns:$index-->";
258 $displayTextHtml = $entry['text'];
259 if ( isset( $entry['selflink'] ) ) {
260 $replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayTextHtml, $query );
261 continue;
262 }
263 if ( $displayTextHtml === '' ) {
264 $displayText = null;
265 } else {
266 $displayText = new HtmlArmor( $displayTextHtml );
267 }
268 if ( !isset( $classes[$pdbk] ) ) {
269 $classes[$pdbk] = 'new';
270 }
271 if ( $classes[$pdbk] == 'new' ) {
272 $linkCache->addBadLinkObj( $title );
273 $output->addLink( $title, 0 );
274 $link = $linkRenderer->makeBrokenLink(
275 $title, $displayText, [], $query
276 );
277 } else {
278 $link = $linkRenderer->makePreloadedLink(
279 $title, $displayText, $classes[$pdbk], [], $query
280 );
281 }
282
283 $replacePairs[$searchkey] = $link;
284 }
285 }
286
287 # Do the thing
288 $text = preg_replace_callback(
289 '/(<!--LINK\'" .*?-->)/',
290 static function ( array $matches ) use ( $replacePairs ) {
291 return $replacePairs[$matches[1]];
292 },
293 $text
294 );
295 }
296
301 protected function replaceInterwiki( &$text ) {
302 if ( empty( $this->interwikis ) ) {
303 return;
304 }
305
306 # Make interwiki link HTML
307 $output = $this->parent->getOutput();
308 $replacePairs = [];
309 $linkRenderer = $this->parent->getLinkRenderer();
310 foreach ( $this->interwikis as $key => $link ) {
311 $replacePairs[$key] = $linkRenderer->makeLink(
312 $link['title'],
313 new HtmlArmor( $link['text'] )
314 );
315 $output->addInterwikiLink( $link['title'] );
316 }
317
318 $text = preg_replace_callback(
319 '/<!--IWLINK\'" (.*?)-->/',
320 static function ( array $matches ) use ( $replacePairs ) {
321 return $replacePairs[$matches[1]];
322 },
323 $text
324 );
325 }
326
331 protected function doVariants( &$classes ) {
332 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
333 $linkBatch = $linkBatchFactory->newLinkBatch();
334 $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
335 $output = $this->parent->getOutput();
336 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
337 $titlesToBeConverted = '';
338 $titlesAttrs = [];
339
340 // Concatenate titles to a single string, thus we only need auto convert the
341 // single string to all variants. This would improve parser's performance
342 // significantly.
343 foreach ( $this->internals as $ns => $entries ) {
344 if ( $ns == NS_SPECIAL ) {
345 continue;
346 }
347 foreach ( $entries as $index => [ 'title' => $title, 'pdbk' => $pdbk ] ) {
348 // we only deal with new links (in its first query)
349 if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
350 $titlesAttrs[] = [ $index, $title ];
351 // separate titles with \0 because it would never appears
352 // in a valid title
353 $titlesToBeConverted .= $title->getText() . "\0";
354 }
355 }
356 }
357
358 // Now do the conversion and explode string to text of titles
359 $titlesAllVariants = $this->languageConverter->
360 autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
361 foreach ( $titlesAllVariants as &$titlesVariant ) {
362 $titlesVariant = explode( "\0", $titlesVariant );
363 }
364
365 // Then add variants of links to link batch
366 $parentTitle = $this->parent->getTitle();
367 foreach ( $titlesAttrs as $i => [ $index, $title ] ) {
369 $ns = $title->getNamespace();
370 $text = $title->getText();
371
372 foreach ( $titlesAllVariants as $variantName => $textVariants ) {
373 $textVariant = $textVariants[$i];
374 if ( $textVariant === $text ) {
375 continue;
376 }
377
378 $variantTitle = Title::makeTitle( $ns, $textVariant );
379
380 // Self-link checking for mixed/different variant titles. At this point, we
381 // already know the exact title does not exist, so the link cannot be to a
382 // variant of the current title that exists as a separate page.
383 if ( $variantTitle->equals( $parentTitle ) && !$title->hasFragment() ) {
384 $this->internals[$ns][$index]['selflink'] = true;
385 continue 2;
386 }
387
388 $linkBatch->addObj( $variantTitle );
389 $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
390 }
391 }
392
393 // process categories, check if a category exists in some variant
394 $categoryMap = []; // maps $category_variant => $category (dbkeys)
395 $varCategories = []; // category replacements oldDBkey => newDBkey
396 foreach ( $output->getCategoryNames() as $category ) {
397 $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
398 $linkBatch->addObj( $categoryTitle );
399 $variants = $this->languageConverter->autoConvertToAllVariants( $category );
400 foreach ( $variants as $variant ) {
401 if ( $variant !== $category ) {
402 $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
403 if ( $variantTitle === null ) {
404 continue;
405 }
406 $linkBatch->addObj( $variantTitle );
407 $categoryMap[$variant] = [ $category, $categoryTitle ];
408 }
409 }
410 }
411
412 if ( !$linkBatch->isEmpty() ) {
413 // construct query
415
416 $varRes = $dbr->newSelectQueryBuilder()
417 ->select( LinkCache::getSelectFields() )
418 ->from( 'page' )
419 ->where( [ $linkBatch->constructSet( 'page', $dbr ) ] )
420 ->caller( __METHOD__ )
421 ->fetchResultSet();
422
423 $pagemap = [];
424 $linkRenderer = $this->parent->getLinkRenderer();
425
426 // for each found variants, figure out link holders and replace
427 foreach ( $varRes as $s ) {
428 $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
429 $varPdbk = $variantTitle->getPrefixedDBkey();
430 $vardbk = $variantTitle->getDBkey();
431
432 $holderKeys = [];
433 if ( isset( $variantMap[$varPdbk] ) ) {
434 $holderKeys = $variantMap[$varPdbk];
435 $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
436 $output->addLink( $variantTitle, $s->page_id );
437 }
438
439 // loop over link holders
440 foreach ( $holderKeys as $key ) {
441 list( $ns, $index ) = explode( ':', $key, 2 );
442 $entry =& $this->internals[$ns][$index];
443 $pdbk = $entry['pdbk'];
444
445 if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
446 // found link in some of the variants, replace the link holder data
447 $entry['title'] = $variantTitle;
448 $entry['pdbk'] = $varPdbk;
449
450 // set pdbk and colour
451 $classes[$varPdbk] = $linkRenderer->getLinkClasses( $variantTitle );
452 $pagemap[$s->page_id] = $pdbk;
453 }
454 }
455
456 // check if the object is a variant of a category
457 if ( isset( $categoryMap[$vardbk] ) ) {
458 list( $oldkey, $oldtitle ) = $categoryMap[$vardbk];
459 if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
460 $varCategories[$oldkey] = $vardbk;
461 }
462 }
463 }
464 $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
465
466 // rebuild the categories in original order (if there are replacements)
467 if ( $varCategories !== [] ) {
468 $newCats = [];
469 $originalCats = $output->getCategories();
470 foreach ( $originalCats as $cat => $sortkey ) {
471 // make the replacement
472 $newCats[$varCategories[$cat] ?? $cat] = $sortkey;
473 }
474 $output->setCategories( $newCats );
475 }
476 }
477 }
478
486 public function replaceText( $text ) {
487 return preg_replace_callback(
488 '/<!--(IW)?LINK\'" (.*?)-->/',
489 function ( $matches ) {
490 list( $unchanged, $isInterwiki, $key ) = $matches;
491
492 if ( !$isInterwiki ) {
493 list( $ns, $index ) = explode( ':', $key, 2 );
494 return $this->internals[$ns][$index]['text'] ?? $unchanged;
495 } else {
496 return $this->interwikis[$key]['text'] ?? $unchanged;
497 }
498 },
499 $text
500 );
501 }
502}
const NS_SPECIAL
Definition Defines.php:53
const NS_CATEGORY
Definition Defines.php:78
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
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).
replaceInternal(&$text)
Replace internal links.
__destruct()
Reduce memory usage to reduce the impact of circular references.
__construct(Parser $parent, ILanguageConverter $languageConverter, HookContainer $hookContainer)
clear()
Clear all stored link holders.
isBig()
Returns true if the memory requirements of this object are getting large.
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:165
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:1796
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:96
Represents a title within MediaWiki.
Definition Title.php:49
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1066
getPrefixedDBkey()
Get the prefixed database key form.
Definition Title.php:1878
isExternal()
Is this Title interwiki?
Definition Title.php:956
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:26