MediaWiki  master
LinkHolderArray.php
Go to the documentation of this file.
1 <?php
25 
31  public $internals = [];
33  public $interwikis = [];
35  public $size = 0;
36 
40  public $parent;
41  protected $tempIdOffset;
42 
46  public function __construct( $parent ) {
47  $this->parent = $parent;
48  }
49 
53  public function __destruct() {
54  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
55  foreach ( $this as $name => $value ) {
56  unset( $this->$name );
57  }
58  }
59 
64  public function merge( $other ) {
65  foreach ( $other->internals as $ns => $entries ) {
66  $this->size += count( $entries );
67  if ( !isset( $this->internals[$ns] ) ) {
68  $this->internals[$ns] = $entries;
69  } else {
70  $this->internals[$ns] += $entries;
71  }
72  }
73  $this->interwikis += $other->interwikis;
74  }
75 
80  public function isBig() {
82  return $this->size > $wgLinkHolderBatchSize;
83  }
84 
89  public function clear() {
90  $this->internals = [];
91  $this->interwikis = [];
92  $this->size = 0;
93  }
94 
108  public function makeHolder( $nt, $text = '', $query = [], $trail = '', $prefix = '' ) {
109  if ( !is_object( $nt ) ) {
110  # Fail gracefully
111  $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
112  } else {
113  # Separate the link trail from the rest of the link
114  list( $inside, $trail ) = Linker::splitTrail( $trail );
115 
116  $entry = [
117  'title' => $nt,
118  'text' => $prefix . $text . $inside,
119  'pdbk' => $nt->getPrefixedDBkey(),
120  ];
121  if ( $query !== [] ) {
122  $entry['query'] = $query;
123  }
124 
125  if ( $nt->isExternal() ) {
126  // Use a globally unique ID to keep the objects mergable
127  $key = $this->parent->nextLinkID();
128  $this->interwikis[$key] = $entry;
129  $retVal = "<!--IWLINK'\" $key-->{$trail}";
130  } else {
131  $key = $this->parent->nextLinkID();
132  $ns = $nt->getNamespace();
133  $this->internals[$ns][$key] = $entry;
134  $retVal = "<!--LINK'\" $ns:$key-->{$trail}";
135  }
136  $this->size++;
137  }
138  return $retVal;
139  }
140 
146  public function replace( &$text ) {
147  $this->replaceInternal( $text );
148  $this->replaceInterwiki( $text );
149  }
150 
156  protected function replaceInternal( &$text ) {
157  if ( !$this->internals ) {
158  return;
159  }
160 
161  $colours = [];
162  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
163  $output = $this->parent->getOutput();
164  $linkRenderer = $this->parent->getLinkRenderer();
165 
166  $dbr = wfGetDB( DB_REPLICA );
167 
168  # Sort by namespace
169  ksort( $this->internals );
170 
171  $linkcolour_ids = [];
172 
173  # Generate query
174  $lb = new LinkBatch();
175  $lb->setCaller( __METHOD__ );
176 
177  foreach ( $this->internals as $ns => $entries ) {
178  foreach ( $entries as $entry ) {
180  $title = $entry['title'];
181  $pdbk = $entry['pdbk'];
182 
183  # Skip invalid entries.
184  # Result will be ugly, but prevents crash.
185  if ( $title === null ) {
186  continue;
187  }
188 
189  # Check if it's a static known link, e.g. interwiki
190  if ( $title->isAlwaysKnown() ) {
191  $colours[$pdbk] = '';
192  } elseif ( $ns == NS_SPECIAL ) {
193  $colours[$pdbk] = 'new';
194  } else {
195  $id = $linkCache->getGoodLinkID( $pdbk );
196  if ( $id != 0 ) {
197  $colours[$pdbk] = $linkRenderer->getLinkClasses( $title );
198  $output->addLink( $title, $id );
199  $linkcolour_ids[$id] = $pdbk;
200  } elseif ( $linkCache->isBadLink( $pdbk ) ) {
201  $colours[$pdbk] = 'new';
202  } else {
203  # Not in the link cache, add it to the query
204  $lb->addObj( $title );
205  }
206  }
207  }
208  }
209  if ( !$lb->isEmpty() ) {
210  $fields = array_merge(
212  [ 'page_namespace', 'page_title' ]
213  );
214 
215  $res = $dbr->select(
216  'page',
217  $fields,
218  $lb->constructSet( 'page', $dbr ),
219  __METHOD__
220  );
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  $colours[$pdbk] = $linkRenderer->getLinkClasses( $title );
230  // add id to the extension todolist
231  $linkcolour_ids[$s->page_id] = $pdbk;
232  }
233  unset( $res );
234  }
235  if ( count( $linkcolour_ids ) ) {
236  // pass an array of page_ids to an extension
237  Hooks::run( 'GetLinkColours', [ $linkcolour_ids, &$colours, $this->parent->getTitle() ] );
238  }
239 
240  # Do a second query for different language variants of links and categories
241  if ( $this->parent->getContentLanguage()->hasVariants() ) {
242  $this->doVariants( $colours );
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  $key = "$ns:$index";
253  $searchkey = "<!--LINK'\" $key-->";
254  $displayText = $entry['text'];
255  if ( isset( $entry['selflink'] ) ) {
256  $replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayText, $query );
257  continue;
258  }
259  if ( $displayText === '' ) {
260  $displayText = null;
261  } else {
262  $displayText = new HtmlArmor( $displayText );
263  }
264  if ( !isset( $colours[$pdbk] ) ) {
265  $colours[$pdbk] = 'new';
266  }
267  $attribs = [];
268  if ( $colours[$pdbk] == 'new' ) {
269  $linkCache->addBadLinkObj( $title );
270  $output->addLink( $title, 0 );
271  $link = $linkRenderer->makeBrokenLink(
272  $title, $displayText, $attribs, $query
273  );
274  } else {
275  $link = $linkRenderer->makePreloadedLink(
276  $title, $displayText, $colours[$pdbk], $attribs, $query
277  );
278  }
279 
280  $replacePairs[$searchkey] = $link;
281  }
282  }
283 
284  # Do the thing
285  $text = preg_replace_callback(
286  '/(<!--LINK\'" .*?-->)/',
287  function ( array $matches ) use ( $replacePairs ) {
288  return $replacePairs[$matches[1]];
289  },
290  $text
291  );
292  }
293 
299  protected function replaceInterwiki( &$text ) {
300  if ( empty( $this->interwikis ) ) {
301  return;
302  }
303 
304  # Make interwiki link HTML
305  $output = $this->parent->getOutput();
306  $replacePairs = [];
307  $linkRenderer = $this->parent->getLinkRenderer();
308  foreach ( $this->interwikis as $key => $link ) {
309  $replacePairs[$key] = $linkRenderer->makeLink(
310  $link['title'],
311  new HtmlArmor( $link['text'] )
312  );
313  $output->addInterwikiLink( $link['title'] );
314  }
315 
316  $text = preg_replace_callback(
317  '/<!--IWLINK\'" (.*?)-->/',
318  function ( array $matches ) use ( $replacePairs ) {
319  return $replacePairs[$matches[1]];
320  },
321  $text
322  );
323  }
324 
329  protected function doVariants( &$colours ) {
330  $linkBatch = new LinkBatch();
331  $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
332  $output = $this->parent->getOutput();
333  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
334  $titlesToBeConverted = '';
335  $titlesAttrs = [];
336 
337  // Concatenate titles to a single string, thus we only need auto convert the
338  // single string to all variants. This would improve parser's performance
339  // significantly.
340  foreach ( $this->internals as $ns => $entries ) {
341  if ( $ns == NS_SPECIAL ) {
342  continue;
343  }
344  foreach ( $entries as $index => $entry ) {
345  $pdbk = $entry['pdbk'];
346  // we only deal with new links (in its first query)
347  if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
348  $titlesAttrs[] = [ $index, $entry['title'] ];
349  // separate titles with \0 because it would never appears
350  // in a valid title
351  $titlesToBeConverted .= $entry['title']->getText() . "\0";
352  }
353  }
354  }
355 
356  // Now do the conversion and explode string to text of titles
357  $titlesAllVariants = $this->parent->getContentLanguage()->
358  autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
359  $allVariantsName = array_keys( $titlesAllVariants );
360  foreach ( $titlesAllVariants as &$titlesVariant ) {
361  $titlesVariant = explode( "\0", $titlesVariant );
362  }
363 
364  // Then add variants of links to link batch
365  $parentTitle = $this->parent->getTitle();
366  foreach ( $titlesAttrs as $i => $attrs ) {
368  list( $index, $title ) = $attrs;
369  $ns = $title->getNamespace();
370  $text = $title->getText();
371 
372  foreach ( $allVariantsName as $variantName ) {
373  $textVariant = $titlesAllVariants[$variantName][$i];
374  if ( $textVariant === $text ) {
375  continue;
376  }
377 
378  $variantTitle = Title::makeTitle( $ns, $textVariant );
379 
380  // Self-link checking for mixed/different variant titles. At this point, we
381  // already know the exact title does not exist, so the link cannot be to a
382  // variant of the current title that exists as a separate page.
383  if ( $variantTitle->equals( $parentTitle ) && !$title->hasFragment() ) {
384  $this->internals[$ns][$index]['selflink'] = true;
385  continue 2;
386  }
387 
388  $linkBatch->addObj( $variantTitle );
389  $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
390  }
391  }
392 
393  // process categories, check if a category exists in some variant
394  $categoryMap = []; // maps $category_variant => $category (dbkeys)
395  $varCategories = []; // category replacements oldDBkey => newDBkey
396  foreach ( $output->getCategoryLinks() as $category ) {
397  $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
398  $linkBatch->addObj( $categoryTitle );
399  $variants = $this->parent->getContentLanguage()->autoConvertToAllVariants( $category );
400  foreach ( $variants as $variant ) {
401  if ( $variant !== $category ) {
402  $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
403  if ( $variantTitle === null ) {
404  continue;
405  }
406  $linkBatch->addObj( $variantTitle );
407  $categoryMap[$variant] = [ $category, $categoryTitle ];
408  }
409  }
410  }
411 
412  if ( !$linkBatch->isEmpty() ) {
413  // construct query
414  $dbr = wfGetDB( DB_REPLICA );
415  $fields = array_merge(
417  [ 'page_namespace', 'page_title' ]
418  );
419 
420  $varRes = $dbr->select( 'page',
421  $fields,
422  $linkBatch->constructSet( 'page', $dbr ),
423  __METHOD__
424  );
425 
426  $linkcolour_ids = [];
427  $linkRenderer = $this->parent->getLinkRenderer();
428 
429  // for each found variants, figure out link holders and replace
430  foreach ( $varRes as $s ) {
431  $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
432  $varPdbk = $variantTitle->getPrefixedDBkey();
433  $vardbk = $variantTitle->getDBkey();
434 
435  $holderKeys = [];
436  if ( isset( $variantMap[$varPdbk] ) ) {
437  $holderKeys = $variantMap[$varPdbk];
438  $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
439  $output->addLink( $variantTitle, $s->page_id );
440  }
441 
442  // loop over link holders
443  foreach ( $holderKeys as $key ) {
444  list( $ns, $index ) = explode( ':', $key, 2 );
445  $entry =& $this->internals[$ns][$index];
446  $pdbk = $entry['pdbk'];
447 
448  if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
449  // found link in some of the variants, replace the link holder data
450  $entry['title'] = $variantTitle;
451  $entry['pdbk'] = $varPdbk;
452 
453  // set pdbk and colour
454  $colours[$varPdbk] = $linkRenderer->getLinkClasses( $variantTitle );
455  $linkcolour_ids[$s->page_id] = $pdbk;
456  }
457  }
458 
459  // check if the object is a variant of a category
460  if ( isset( $categoryMap[$vardbk] ) ) {
461  list( $oldkey, $oldtitle ) = $categoryMap[$vardbk];
462  if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
463  $varCategories[$oldkey] = $vardbk;
464  }
465  }
466  }
467  Hooks::run( 'GetLinkColours', [ $linkcolour_ids, &$colours, $this->parent->getTitle() ] );
468 
469  // rebuild the categories in original order (if there are replacements)
470  if ( count( $varCategories ) > 0 ) {
471  $newCats = [];
472  $originalCats = $output->getCategories();
473  foreach ( $originalCats as $cat => $sortkey ) {
474  // make the replacement
475  if ( array_key_exists( $cat, $varCategories ) ) {
476  $newCats[$varCategories[$cat]] = $sortkey;
477  } else {
478  $newCats[$cat] = $sortkey;
479  }
480  }
481  $output->setCategoryLinks( $newCats );
482  }
483  }
484  }
485 
493  public function replaceText( $text ) {
494  return preg_replace_callback(
495  '/<!--(IW)?LINK\'" (.*?)-->/',
496  function ( $matches ) {
497  list( $unchanged, $isInterwiki, $key ) = $matches;
498 
499  if ( !$isInterwiki ) {
500  list( $ns, $index ) = explode( ':', $key, 2 );
501  return $this->internals[$ns][$index]['text'] ?? $unchanged;
502  } else {
503  return $this->interwikis[$key]['text'] ?? $unchanged;
504  }
505  },
506  $text
507  );
508  }
509 }
LinkHolderArray\replaceInterwiki
replaceInterwiki(&$text)
Replace interwiki links.
Definition: LinkHolderArray.php:299
LinkHolderArray\isBig
isBig()
Returns true if the memory requirements of this object are getting large.
Definition: LinkHolderArray.php:80
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:28
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:130
Linker\makeSelfLinkObj
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:164
LinkHolderArray\clear
clear()
Clear all stored link holders.
Definition: LinkHolderArray.php:89
LinkHolderArray\$size
int $size
Definition: LinkHolderArray.php:35
LinkHolderArray\merge
merge( $other)
Merge another LinkHolderArray into this one.
Definition: LinkHolderArray.php:64
$s
$s
Definition: mergeMessageFileList.php:185
LinkHolderArray\replaceText
replaceText( $text)
Replace and link placeholders with plain text of links (not HTML-formatted).
Definition: LinkHolderArray.php:493
$res
$res
Definition: testCompression.php:54
LinkHolderArray\replace
replace(&$text)
Replace link placeholders with actual links, in the buffer.
Definition: LinkHolderArray.php:146
LinkCache\getSelectFields
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:219
$dbr
$dbr
Definition: testCompression.php:52
LinkHolderArray\__destruct
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: LinkHolderArray.php:53
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:49
$wgLinkHolderBatchSize
$wgLinkHolderBatchSize
LinkHolderArray batch size For debugging.
Definition: DefaultSettings.php:8506
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2562
$matches
$matches
Definition: NoLocalSettings.php:24
LinkHolderArray
Definition: LinkHolderArray.php:29
$title
$title
Definition: testCompression.php:36
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
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:156
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:74
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:1772
LinkHolderArray\doVariants
doVariants(&$colours)
Modify $this->internals and $colours according to language variant linking rules.
Definition: LinkHolderArray.php:329
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:610
LinkHolderArray\$tempIdOffset
$tempIdOffset
Definition: LinkHolderArray.php:41
Title
Represents a title within MediaWiki.
Definition: Title.php:42
LinkHolderArray\$parent
Parser $parent
Definition: LinkHolderArray.php:40
LinkHolderArray\$interwikis
array[] $interwikis
Definition: LinkHolderArray.php:33
LinkHolderArray\__construct
__construct( $parent)
Definition: LinkHolderArray.php:46
LinkHolderArray\makeHolder
makeHolder( $nt, $text='', $query=[], $trail='', $prefix='')
Make a link placeholder.
Definition: LinkHolderArray.php:108
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
LinkHolderArray\$internals
array[][] $internals
Definition: LinkHolderArray.php:31