MediaWiki master
LinkHolderArray.php
Go to the documentation of this file.
1<?php
33
41 private $internals = [];
43 private $interwikis = [];
45 private $size = 0;
47 private $parent;
49 private $languageConverter;
51 private $hookRunner;
52
58 public function __construct( Parser $parent, ILanguageConverter $languageConverter,
59 HookContainer $hookContainer
60 ) {
61 $this->parent = $parent;
62 $this->languageConverter = $languageConverter;
63 $this->hookRunner = new HookRunner( $hookContainer );
64 }
65
69 public function __destruct() {
70 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
71 foreach ( $this as $name => $_ ) {
72 unset( $this->$name );
73 }
74 }
75
80 public function merge( $other ) {
81 foreach ( $other->internals as $ns => $entries ) {
82 $this->size += count( $entries );
83 if ( !isset( $this->internals[$ns] ) ) {
84 $this->internals[$ns] = $entries;
85 } else {
86 $this->internals[$ns] += $entries;
87 }
88 }
89 $this->interwikis += $other->interwikis;
90 }
91
96 public function isBig() {
97 $linkHolderBatchSize = MediaWikiServices::getInstance()->getMainConfig()
98 ->get( MainConfigNames::LinkHolderBatchSize );
99 return $this->size > $linkHolderBatchSize;
100 }
101
106 public function clear() {
107 $this->internals = [];
108 $this->interwikis = [];
109 $this->size = 0;
110 }
111
124 public function makeHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
125 # Separate the link trail from the rest of the link
126 [ $inside, $trail ] = Linker::splitTrail( $trail );
127
128 $key = $this->parent->nextLinkID();
129 $entry = [
130 'title' => $nt,
131 'text' => $prefix . $text . $inside,
132 'pdbk' => $nt->getPrefixedDBkey(),
133 ];
134
135 $this->size++;
136 if ( $nt->isExternal() ) {
137 // Use a globally unique ID to keep the objects mergable
138 $this->interwikis[$key] = $entry;
139 return "<!--IWLINK'\" $key-->{$trail}";
140 } else {
141 $ns = $nt->getNamespace();
142 $this->internals[$ns][$key] = $entry;
143 return "<!--LINK'\" $ns:$key-->{$trail}";
144 }
145 }
146
152 public function replace( &$text ) {
153 $this->replaceInternal( $text );
154 $this->replaceInterwiki( $text );
155 }
156
161 protected function replaceInternal( &$text ) {
162 if ( !$this->internals ) {
163 return;
164 }
165
166 $classes = [];
167 $services = MediaWikiServices::getInstance();
168 $linkCache = $services->getLinkCache();
169 $output = $this->parent->getOutput();
170 $linkRenderer = $this->parent->getLinkRenderer();
171
172 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
173
174 # Sort by namespace
175 ksort( $this->internals );
176
177 $pagemap = [];
178
179 # Generate query
180 $linkBatchFactory = $services->getLinkBatchFactory();
181 $lb = $linkBatchFactory->newLinkBatch();
182 $lb->setCaller( __METHOD__ );
183
184 foreach ( $this->internals as $ns => $entries ) {
185 foreach ( $entries as [ 'title' => $title, 'pdbk' => $pdbk ] ) {
187 # Check if it's a static known link, e.g. interwiki
188 if ( $title->isAlwaysKnown() ) {
189 $classes[$pdbk] = '';
190 } elseif ( $ns === NS_SPECIAL ) {
191 $classes[$pdbk] = 'new';
192 } else {
193 $id = $linkCache->getGoodLinkID( $pdbk );
194 if ( $id ) {
195 $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
196 $output->addLink( $title, $id );
197 $pagemap[$id] = $pdbk;
198 } elseif ( $linkCache->isBadLink( $pdbk ) ) {
199 $classes[$pdbk] = 'new';
200 } else {
201 # Not in the link cache, add it to the query
202 $lb->addObj( $title );
203 }
204 }
205 }
206 }
207 if ( !$lb->isEmpty() ) {
208 $res = $dbr->newSelectQueryBuilder()
209 ->select( LinkCache::getSelectFields() )
210 ->from( 'page' )
211 ->where( [ $lb->constructSet( 'page', $dbr ) ] )
212 ->caller( __METHOD__ )
213 ->fetchResultSet();
214
215 # Fetch data and form into an associative array
216 # non-existent = broken
217 foreach ( $res as $s ) {
218 $title = Title::makeTitle( $s->page_namespace, $s->page_title );
219 $pdbk = $title->getPrefixedDBkey();
220 $linkCache->addGoodLinkObjFromRow( $title, $s );
221 $output->addLink( $title, $s->page_id );
222 $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
223 // add id to the extension todolist
224 $pagemap[$s->page_id] = $pdbk;
225 }
226 unset( $res );
227 }
228 if ( $pagemap !== [] ) {
229 // pass an array of page_ids to an extension
230 $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
231 }
232
233 # Do a second query for different language variants of links and categories
234 if ( $this->languageConverter->hasVariants() ) {
235 $this->doVariants( $classes );
236 }
237
238 # Construct search and replace arrays
239 $replacePairs = [];
240 foreach ( $this->internals as $ns => $entries ) {
241 foreach ( $entries as $index => $entry ) {
242 $pdbk = $entry['pdbk'];
243 $title = $entry['title'];
244 $query = $entry['query'] ?? [];
245 $searchkey = "$ns:$index";
246 $displayTextHtml = $entry['text'];
247 if ( isset( $entry['selflink'] ) ) {
248 $replacePairs[$searchkey] = Linker::makeSelfLinkObj(
249 $title, $displayTextHtml, $query, '', '',
250 Sanitizer::escapeIdForLink( $title->getFragment() )
251 );
252 continue;
253 }
254
255 $displayText = $displayTextHtml === '' ? null : new HtmlArmor( $displayTextHtml );
256 if ( !isset( $classes[$pdbk] ) ) {
257 $classes[$pdbk] = 'new';
258 }
259 if ( $classes[$pdbk] === 'new' ) {
260 $linkCache->addBadLinkObj( $title );
261 $output->addLink( $title, 0 );
262 $link = $linkRenderer->makeBrokenLink(
263 $title, $displayText, [], $query
264 );
265 } else {
266 $link = $linkRenderer->makePreloadedLink(
267 $title, $displayText, $classes[$pdbk], [], $query
268 );
269 }
270
271 $replacePairs[$searchkey] = $link;
272 }
273 }
274
275 # Do the thing
276 $text = preg_replace_callback(
277 '/<!--LINK\'" (-?[\d:]+)-->/',
278 static function ( array $matches ) use ( $replacePairs ) {
279 return $replacePairs[$matches[1]];
280 },
281 $text
282 );
283 }
284
289 protected function replaceInterwiki( &$text ) {
290 if ( !$this->interwikis ) {
291 return;
292 }
293
294 # Make interwiki link HTML
295 $output = $this->parent->getOutput();
296 $replacePairs = [];
297 $linkRenderer = $this->parent->getLinkRenderer();
298 foreach ( $this->interwikis as $key => [ 'title' => $title, 'text' => $linkText ] ) {
299 $replacePairs[$key] = $linkRenderer->makeLink( $title, new HtmlArmor( $linkText ) );
300 $output->addInterwikiLink( $title );
301 }
302
303 $text = preg_replace_callback(
304 '/<!--IWLINK\'" (\d+)-->/',
305 static function ( array $matches ) use ( $replacePairs ) {
306 return $replacePairs[$matches[1]];
307 },
308 $text
309 );
310 }
311
316 protected function doVariants( &$classes ) {
317 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
318 $linkBatch = $linkBatchFactory->newLinkBatch();
319 $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
320 $output = $this->parent->getOutput();
321 $titlesToBeConverted = '';
322 $titlesAttrs = [];
323
324 // Concatenate titles to a single string, thus we only need auto convert the
325 // single string to all variants. This would improve parser's performance
326 // significantly.
327 foreach ( $this->internals as $ns => $entries ) {
328 if ( $ns === NS_SPECIAL ) {
329 continue;
330 }
331 foreach ( $entries as $index => [ 'title' => $title, 'pdbk' => $pdbk ] ) {
332 // we only deal with new links (in its first query)
333 if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
334 $titlesAttrs[] = [ $index, $title ];
335 // separate titles with \0 because it would never appears
336 // in a valid title
337 $titlesToBeConverted .= $title->getText() . "\0";
338 }
339 }
340 }
341
342 // Now do the conversion and explode string to text of titles
343 $titlesAllVariants = $this->languageConverter->
344 autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
345 foreach ( $titlesAllVariants as &$titlesVariant ) {
346 $titlesVariant = explode( "\0", $titlesVariant );
347 }
348
349 // Then add variants of links to link batch
350 $parentTitle = $this->parent->getTitle();
351 foreach ( $titlesAttrs as $i => [ $index, $title ] ) {
353 $ns = $title->getNamespace();
354 $text = $title->getText();
355
356 foreach ( $titlesAllVariants as $textVariants ) {
357 $textVariant = $textVariants[$i];
358 if ( $textVariant === $text ) {
359 continue;
360 }
361
362 $variantTitle = Title::makeTitle( $ns, $textVariant );
363
364 // Self-link checking for mixed/different variant titles. At this point, we
365 // already know the exact title does not exist, so the link cannot be to a
366 // variant of the current title that exists as a separate page.
367 if ( $variantTitle->equals( $parentTitle ) ) {
368 $this->internals[$ns][$index]['selflink'] = true;
369 continue 2;
370 }
371
372 $linkBatch->addObj( $variantTitle );
373 $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
374 }
375 }
376
377 // process categories, check if a category exists in some variant
378 $categoryMap = []; // maps $category_variant => $category (dbkeys)
379 foreach ( $output->getCategoryNames() as $category ) {
380 $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
381 $linkBatch->addObj( $categoryTitle );
382 $variants = $this->languageConverter->autoConvertToAllVariants( $category );
383 foreach ( $variants as $variant ) {
384 if ( $variant !== $category ) {
385 $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
386 if ( $variantTitle ) {
387 $linkBatch->addObj( $variantTitle );
388 $categoryMap[$variant] = [ $category, $categoryTitle ];
389 }
390 }
391 }
392 }
393
394 if ( $linkBatch->isEmpty() ) {
395 return;
396 }
397
398 // construct query
399 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
400
401 $varRes = $dbr->newSelectQueryBuilder()
402 ->select( LinkCache::getSelectFields() )
403 ->from( 'page' )
404 ->where( [ $linkBatch->constructSet( 'page', $dbr ) ] )
405 ->caller( __METHOD__ )
406 ->fetchResultSet();
407
408 $pagemap = [];
409 $varCategories = [];
410 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
411 $linkRenderer = $this->parent->getLinkRenderer();
412
413 // for each found variants, figure out link holders and replace
414 foreach ( $varRes as $s ) {
415 $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
416 $varPdbk = $variantTitle->getPrefixedDBkey();
417
418 if ( !isset( $variantMap[$varPdbk] ) ) {
419 continue;
420 }
421
422 $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
423 $output->addLink( $variantTitle, $s->page_id );
424
425 // loop over link holders
426 foreach ( $variantMap[$varPdbk] as $key ) {
427 [ $ns, $index ] = explode( ':', $key, 2 );
428 $entry =& $this->internals[(int)$ns][(int)$index];
429
430 // The selflink we marked above might not have been the first
431 // $textVariants so be sure to skip any entries that have
432 // subsequently been marked.
433 if ( isset( $entry['selflink'] ) ) {
434 continue;
435 }
436
437 $pdbk = $entry['pdbk'];
438 if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
439 // found link in some of the variants, replace the link holder data
440 $entry['title'] = $variantTitle;
441 $entry['pdbk'] = $varPdbk;
442
443 // set pdbk and colour if we haven't checked this title yet.
444 if ( !isset( $classes[$varPdbk] ) ) {
445 $classes[$varPdbk] = $linkRenderer->getLinkClasses( $variantTitle );
446 $pagemap[$s->page_id] = $varPdbk;
447 }
448 }
449 }
450
451 // check if the object is a variant of a category
452 $vardbk = $variantTitle->getDBkey();
453 if ( isset( $categoryMap[$vardbk] ) ) {
454 [ $oldkey, $oldtitle ] = $categoryMap[$vardbk];
455 if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
456 $varCategories[$oldkey] = $vardbk;
457 }
458 }
459 }
460 $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
461
462 // rebuild the categories in original order (if there are replacements)
463 if ( $varCategories !== [] ) {
464 $newCats = [];
465 foreach ( $output->getCategoryNames() as $cat ) {
466 $sortkey = $output->getCategorySortKey( $cat );
467 // make the replacement
468 $newCats[$varCategories[$cat] ?? $cat] = $sortkey;
469 }
470 $output->setCategories( $newCats );
471 }
472 }
473
481 public function replaceText( $text ) {
482 return preg_replace_callback(
483 '/<!--(IW)?LINK\'" (-?[\d:]+)-->/',
484 function ( $matches ) {
485 [ $unchanged, $isInterwiki, $key ] = $matches;
486
487 if ( !$isInterwiki ) {
488 [ $ns, $index ] = explode( ':', $key, 2 );
489 return $this->internals[(int)$ns][(int)$index]['text'] ?? $unchanged;
490 } else {
491 return $this->interwikis[$key]['text'] ?? $unchanged;
492 }
493 },
494 $text
495 );
496 }
497}
const NS_SPECIAL
Definition Defines.php:54
const NS_CATEGORY
Definition Defines.php:79
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.
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition LinkCache.php:52
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Some internal bits split of from Skin.php.
Definition Linker.php:63
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:155
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Represents a title within MediaWiki.
Definition Title.php:79
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1045
getPrefixedDBkey()
Get the prefixed database key form.
Definition Title.php:1850
The shared interface for all language converters.
isExternal()
Whether this LinkTarget has an interwiki component.