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