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