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