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