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