MediaWiki  master
LinkHolderArray.php
Go to the documentation of this file.
1 <?php
27 
35  public $internals = [];
37  public $interwikis = [];
39  public $size = 0;
40 
44  public $parent;
45 
51 
55  private $hookRunner;
56 
63  HookContainer $hookContainer = null
64  ) {
65  $this->parent = $parent;
66 
67  if ( !$languageConverter ) {
68  wfDeprecated( __METHOD__ . ' without $languageConverter parameter', '1.35' );
69  $languageConverter = MediaWikiServices::getInstance()
70  ->getLanguageConverterFactory()
71  ->getLanguageConverter( $parent->getTargetLanguage() );
72  }
73  $this->languageConverter = $languageConverter;
74  if ( !$hookContainer ) {
75  wfDeprecated( __METHOD__ . ' without $hookContainer parameter', '1.35' );
76  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
77  }
78  $this->hookRunner = new HookRunner( $hookContainer );
79  }
80 
84  public function __destruct() {
85  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
86  foreach ( $this as $name => $_ ) {
87  unset( $this->$name );
88  }
89  }
90 
95  public function merge( $other ) {
96  foreach ( $other->internals as $ns => $entries ) {
97  $this->size += count( $entries );
98  if ( !isset( $this->internals[$ns] ) ) {
99  $this->internals[$ns] = $entries;
100  } else {
101  $this->internals[$ns] += $entries;
102  }
103  }
104  $this->interwikis += $other->interwikis;
105  }
106 
111  public function isBig() {
112  global $wgLinkHolderBatchSize;
113  return $this->size > $wgLinkHolderBatchSize;
114  }
115 
120  public function clear() {
121  $this->internals = [];
122  $this->interwikis = [];
123  $this->size = 0;
124  }
125 
138  public function makeHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
139  # Separate the link trail from the rest of the link
140  list( $inside, $trail ) = Linker::splitTrail( $trail );
141 
142  $key = $this->parent->nextLinkID();
143  $entry = [
144  'title' => $nt,
145  'text' => $prefix . $text . $inside,
146  'pdbk' => $nt->getPrefixedDBkey(),
147  ];
148 
149  $this->size++;
150  if ( $nt->isExternal() ) {
151  // Use a globally unique ID to keep the objects mergable
152  $this->interwikis[$key] = $entry;
153  return "<!--IWLINK'\" $key-->{$trail}";
154  } else {
155  $ns = $nt->getNamespace();
156  $this->internals[$ns][$key] = $entry;
157  return "<!--LINK'\" $ns:$key-->{$trail}";
158  }
159  }
160 
166  public function replace( &$text ) {
167  $this->replaceInternal( $text );
168  $this->replaceInterwiki( $text );
169  }
170 
175  protected function replaceInternal( &$text ) {
176  if ( !$this->internals ) {
177  return;
178  }
179 
180  $classes = [];
181  $services = MediaWikiServices::getInstance();
182  $linkCache = $services->getLinkCache();
183  $output = $this->parent->getOutput();
184  $linkRenderer = $this->parent->getLinkRenderer();
185 
186  $dbr = wfGetDB( DB_REPLICA );
187 
188  # Sort by namespace
189  ksort( $this->internals );
190 
191  $pagemap = [];
192 
193  # Generate query
194  $linkBatchFactory = $services->getLinkBatchFactory();
195  $lb = $linkBatchFactory->newLinkBatch();
196  $lb->setCaller( __METHOD__ );
197 
198  foreach ( $this->internals as $ns => $entries ) {
199  foreach ( $entries as [ 'title' => $title, 'pdbk' => $pdbk ] ) {
202  # Skip invalid entries.
203  # Result will be ugly, but prevents crash.
204  if ( $title === null ) {
205  continue;
206  }
207 
208  # Check if it's a static known link, e.g. interwiki
209  if ( $title->isAlwaysKnown() ) {
210  $classes[$pdbk] = '';
211  } elseif ( $ns == NS_SPECIAL ) {
212  $classes[$pdbk] = 'new';
213  } else {
214  $id = $linkCache->getGoodLinkID( $pdbk );
215  if ( $id != 0 ) {
216  $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
217  $output->addLink( $title, $id );
218  $pagemap[$id] = $pdbk;
219  } elseif ( $linkCache->isBadLink( $pdbk ) ) {
220  $classes[$pdbk] = 'new';
221  } else {
222  # Not in the link cache, add it to the query
223  $lb->addObj( $title );
224  }
225  }
226  }
227  }
228  if ( !$lb->isEmpty() ) {
229  $fields = array_merge(
231  [ 'page_namespace', 'page_title' ]
232  );
233 
234  $res = $dbr->select(
235  'page',
236  $fields,
237  $lb->constructSet( 'page', $dbr ),
238  __METHOD__
239  );
240 
241  # Fetch data and form into an associative array
242  # non-existent = broken
243  foreach ( $res as $s ) {
244  $title = Title::makeTitle( $s->page_namespace, $s->page_title );
245  $pdbk = $title->getPrefixedDBkey();
246  $linkCache->addGoodLinkObjFromRow( $title, $s );
247  $output->addLink( $title, $s->page_id );
248  $classes[$pdbk] = $linkRenderer->getLinkClasses( $title );
249  // add id to the extension todolist
250  $pagemap[$s->page_id] = $pdbk;
251  }
252  unset( $res );
253  }
254  if ( $pagemap !== [] ) {
255  // pass an array of page_ids to an extension
256  $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
257  }
258 
259  # Do a second query for different language variants of links and categories
260  if ( $this->languageConverter->hasVariants() ) {
261  $this->doVariants( $classes );
262  }
263 
264  # Construct search and replace arrays
265  $replacePairs = [];
266  foreach ( $this->internals as $ns => $entries ) {
267  foreach ( $entries as $index => $entry ) {
268  $pdbk = $entry['pdbk'];
269  $title = $entry['title'];
270  $query = $entry['query'] ?? [];
271  $searchkey = "<!--LINK'\" $ns:$index-->";
272  $displayTextHtml = $entry['text'];
273  if ( isset( $entry['selflink'] ) ) {
274  $replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayTextHtml, $query );
275  continue;
276  }
277  if ( $displayTextHtml === '' ) {
278  $displayText = null;
279  } else {
280  $displayText = new HtmlArmor( $displayTextHtml );
281  }
282  if ( !isset( $classes[$pdbk] ) ) {
283  $classes[$pdbk] = 'new';
284  }
285  if ( $classes[$pdbk] == 'new' ) {
286  $linkCache->addBadLinkObj( $title );
287  $output->addLink( $title, 0 );
288  $link = $linkRenderer->makeBrokenLink(
289  $title, $displayText, [], $query
290  );
291  } else {
292  $link = $linkRenderer->makePreloadedLink(
293  $title, $displayText, $classes[$pdbk], [], $query
294  );
295  }
296 
297  $replacePairs[$searchkey] = $link;
298  }
299  }
300 
301  # Do the thing
302  $text = preg_replace_callback(
303  '/(<!--LINK\'" .*?-->)/',
304  static function ( array $matches ) use ( $replacePairs ) {
305  return $replacePairs[$matches[1]];
306  },
307  $text
308  );
309  }
310 
315  protected function replaceInterwiki( &$text ) {
316  if ( empty( $this->interwikis ) ) {
317  return;
318  }
319 
320  # Make interwiki link HTML
321  $output = $this->parent->getOutput();
322  $replacePairs = [];
323  $linkRenderer = $this->parent->getLinkRenderer();
324  foreach ( $this->interwikis as $key => $link ) {
325  $replacePairs[$key] = $linkRenderer->makeLink(
326  $link['title'],
327  new HtmlArmor( $link['text'] )
328  );
329  $output->addInterwikiLink( $link['title'] );
330  }
331 
332  $text = preg_replace_callback(
333  '/<!--IWLINK\'" (.*?)-->/',
334  static function ( array $matches ) use ( $replacePairs ) {
335  return $replacePairs[$matches[1]];
336  },
337  $text
338  );
339  }
340 
345  protected function doVariants( &$classes ) {
346  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
347  $linkBatch = $linkBatchFactory->newLinkBatch();
348  $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
349  $output = $this->parent->getOutput();
350  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
351  $titlesToBeConverted = '';
352  $titlesAttrs = [];
353 
354  // Concatenate titles to a single string, thus we only need auto convert the
355  // single string to all variants. This would improve parser's performance
356  // significantly.
357  foreach ( $this->internals as $ns => $entries ) {
358  if ( $ns == NS_SPECIAL ) {
359  continue;
360  }
361  foreach ( $entries as $index => [ 'title' => $title, 'pdbk' => $pdbk ] ) {
362  // we only deal with new links (in its first query)
363  if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
364  $titlesAttrs[] = [ $index, $title ];
365  // separate titles with \0 because it would never appears
366  // in a valid title
367  $titlesToBeConverted .= $title->getText() . "\0";
368  }
369  }
370  }
371 
372  // Now do the conversion and explode string to text of titles
373  $titlesAllVariants = $this->languageConverter->
374  autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
375  foreach ( $titlesAllVariants as &$titlesVariant ) {
376  $titlesVariant = explode( "\0", $titlesVariant );
377  }
378 
379  // Then add variants of links to link batch
380  $parentTitle = $this->parent->getTitle();
381  foreach ( $titlesAttrs as $i => [ $index, $title ] ) {
383  $ns = $title->getNamespace();
384  $text = $title->getText();
385 
386  foreach ( $titlesAllVariants as $variantName => $textVariants ) {
387  $textVariant = $textVariants[$i];
388  if ( $textVariant === $text ) {
389  continue;
390  }
391 
392  $variantTitle = Title::makeTitle( $ns, $textVariant );
393 
394  // Self-link checking for mixed/different variant titles. At this point, we
395  // already know the exact title does not exist, so the link cannot be to a
396  // variant of the current title that exists as a separate page.
397  if ( $variantTitle->equals( $parentTitle ) && !$title->hasFragment() ) {
398  $this->internals[$ns][$index]['selflink'] = true;
399  continue 2;
400  }
401 
402  $linkBatch->addObj( $variantTitle );
403  $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
404  }
405  }
406 
407  // process categories, check if a category exists in some variant
408  $categoryMap = []; // maps $category_variant => $category (dbkeys)
409  $varCategories = []; // category replacements oldDBkey => newDBkey
410  foreach ( $output->getCategoryNames() as $category ) {
411  $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
412  $linkBatch->addObj( $categoryTitle );
413  $variants = $this->languageConverter->autoConvertToAllVariants( $category );
414  foreach ( $variants as $variant ) {
415  if ( $variant !== $category ) {
416  $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
417  if ( $variantTitle === null ) {
418  continue;
419  }
420  $linkBatch->addObj( $variantTitle );
421  $categoryMap[$variant] = [ $category, $categoryTitle ];
422  }
423  }
424  }
425 
426  if ( !$linkBatch->isEmpty() ) {
427  // construct query
428  $dbr = wfGetDB( DB_REPLICA );
429  $fields = array_merge(
431  [ 'page_namespace', 'page_title' ]
432  );
433 
434  $varRes = $dbr->select( 'page',
435  $fields,
436  $linkBatch->constructSet( 'page', $dbr ),
437  __METHOD__
438  );
439 
440  $pagemap = [];
441  $linkRenderer = $this->parent->getLinkRenderer();
442 
443  // for each found variants, figure out link holders and replace
444  foreach ( $varRes as $s ) {
445  $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
446  $varPdbk = $variantTitle->getPrefixedDBkey();
447  $vardbk = $variantTitle->getDBkey();
448 
449  $holderKeys = [];
450  if ( isset( $variantMap[$varPdbk] ) ) {
451  $holderKeys = $variantMap[$varPdbk];
452  $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
453  $output->addLink( $variantTitle, $s->page_id );
454  }
455 
456  // loop over link holders
457  foreach ( $holderKeys as $key ) {
458  list( $ns, $index ) = explode( ':', $key, 2 );
459  $entry =& $this->internals[$ns][$index];
460  $pdbk = $entry['pdbk'];
461 
462  if ( !isset( $classes[$pdbk] ) || $classes[$pdbk] === 'new' ) {
463  // found link in some of the variants, replace the link holder data
464  $entry['title'] = $variantTitle;
465  $entry['pdbk'] = $varPdbk;
466 
467  // set pdbk and colour
468  $classes[$varPdbk] = $linkRenderer->getLinkClasses( $variantTitle );
469  $pagemap[$s->page_id] = $pdbk;
470  }
471  }
472 
473  // check if the object is a variant of a category
474  if ( isset( $categoryMap[$vardbk] ) ) {
475  list( $oldkey, $oldtitle ) = $categoryMap[$vardbk];
476  if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
477  $varCategories[$oldkey] = $vardbk;
478  }
479  }
480  }
481  $this->hookRunner->onGetLinkColours( $pagemap, $classes, $this->parent->getTitle() );
482 
483  // rebuild the categories in original order (if there are replacements)
484  if ( $varCategories !== [] ) {
485  $newCats = [];
486  $originalCats = $output->getCategories();
487  foreach ( $originalCats as $cat => $sortkey ) {
488  // make the replacement
489  $newCats[$varCategories[$cat] ?? $cat] = $sortkey;
490  }
491  $output->setCategories( $newCats );
492  }
493  }
494  }
495 
503  public function replaceText( $text ) {
504  return preg_replace_callback(
505  '/<!--(IW)?LINK\'" (.*?)-->/',
506  function ( $matches ) {
507  list( $unchanged, $isInterwiki, $key ) = $matches;
508 
509  if ( !$isInterwiki ) {
510  list( $ns, $index ) = explode( ':', $key, 2 );
511  return $this->internals[$ns][$index]['text'] ?? $unchanged;
512  } else {
513  return $this->interwikis[$key]['text'] ?? $unchanged;
514  }
515  },
516  $text
517  );
518  }
519 }
LinkHolderArray\replaceInterwiki
replaceInterwiki(&$text)
Replace interwiki links.
Definition: LinkHolderArray.php:315
LinkHolderArray\isBig
isBig()
Returns true if the memory requirements of this object are getting large.
Definition: LinkHolderArray.php:111
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:200
Linker\makeSelfLinkObj
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:161
LinkHolderArray\doVariants
doVariants(&$classes)
Modify $this->internals and $classes according to language variant linking rules.
Definition: LinkHolderArray.php:345
Title\getPrefixedDBkey
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1899
LinkHolderArray\clear
clear()
Clear all stored link holders.
Definition: LinkHolderArray.php:120
LinkHolderArray\$languageConverter
ILanguageConverter $languageConverter
Current language converter.
Definition: LinkHolderArray.php:50
LinkHolderArray\$size
int $size
Definition: LinkHolderArray.php:39
LinkHolderArray\merge
merge( $other)
Merge another LinkHolderArray into this one.
Definition: LinkHolderArray.php:95
Parser\getTargetLanguage
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:1150
Title\isExternal
isExternal()
Is this Title interwiki?
Definition: Title.php:971
LinkHolderArray\replaceText
replaceText( $text)
Replace and link placeholders with plain text of links (not HTML-formatted).
Definition: LinkHolderArray.php:503
$res
$res
Definition: testCompression.php:57
LinkHolderArray\replace
replace(&$text)
Replace link placeholders with actual links, in the buffer.
Definition: LinkHolderArray.php:166
LinkCache\getSelectFields
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:383
$dbr
$dbr
Definition: testCompression.php:54
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:53
LinkHolderArray\__destruct
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: LinkHolderArray.php:84
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
$wgLinkHolderBatchSize
$wgLinkHolderBatchSize
LinkHolderArray batch size For debugging.
Definition: DefaultSettings.php:2723
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1081
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2200
$matches
$matches
Definition: NoLocalSettings.php:24
LinkHolderArray
Definition: LinkHolderArray.php:33
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:663
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
LinkHolderArray\replaceInternal
replaceInternal(&$text)
Replace internal links.
Definition: LinkHolderArray.php:175
Linker\splitTrail
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition: Linker.php:1719
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:689
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
ILanguageConverter
The shared interface for all language converters.
Definition: ILanguageConverter.php:29
LinkHolderArray\$hookRunner
HookRunner $hookRunner
Definition: LinkHolderArray.php:55
LinkHolderArray\__construct
__construct(Parser $parent, ILanguageConverter $languageConverter=null, HookContainer $hookContainer=null)
Definition: LinkHolderArray.php:62
Parser
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:92
Title
Represents a title within MediaWiki.
Definition: Title.php:47
LinkHolderArray\$parent
Parser $parent
Definition: LinkHolderArray.php:44
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
LinkHolderArray\$interwikis
array[] $interwikis
Definition: LinkHolderArray.php:37
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:554
LinkHolderArray\makeHolder
makeHolder(Title $nt, $text='', $trail='', $prefix='')
Make a link placeholder.
Definition: LinkHolderArray.php:138
LinkHolderArray\$internals
array[][] $internals
Definition: LinkHolderArray.php:35