MediaWiki  master
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 
179  $dbr = wfGetDB( DB_REPLICA );
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
403  $dbr = wfGetDB( DB_REPLICA );
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.
$matches
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:374
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...
Definition: HookRunner.php:568
Some internal bits split of from Skin.php.
Definition: Linker.php:65
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:1909
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.
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
const DB_REPLICA
Definition: defines.php:26