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