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