MediaWiki  1.34.0
LinkHolderArray.php
Go to the documentation of this file.
1 <?php
25 
30  public $internals = [];
31  public $interwikis = [];
32  public $size = 0;
33 
37  public $parent;
38  protected $tempIdOffset;
39 
43  public function __construct( $parent ) {
44  $this->parent = $parent;
45  }
46 
50  public function __destruct() {
51  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
52  foreach ( $this as $name => $value ) {
53  unset( $this->$name );
54  }
55  }
56 
65  public function __sleep() {
66  foreach ( $this->internals as &$nsLinks ) {
67  foreach ( $nsLinks as &$entry ) {
68  unset( $entry['title'] );
69  }
70  }
71  unset( $nsLinks );
72  unset( $entry );
73 
74  foreach ( $this->interwikis as &$entry ) {
75  unset( $entry['title'] );
76  }
77  unset( $entry );
78 
79  return [ 'internals', 'interwikis', 'size' ];
80  }
81 
85  public function __wakeup() {
86  foreach ( $this->internals as &$nsLinks ) {
87  foreach ( $nsLinks as &$entry ) {
88  $entry['title'] = Title::newFromText( $entry['pdbk'] );
89  }
90  }
91  unset( $nsLinks );
92  unset( $entry );
93 
94  foreach ( $this->interwikis as &$entry ) {
95  $entry['title'] = Title::newFromText( $entry['pdbk'] );
96  }
97  unset( $entry );
98  }
99 
104  public function merge( $other ) {
105  foreach ( $other->internals as $ns => $entries ) {
106  $this->size += count( $entries );
107  if ( !isset( $this->internals[$ns] ) ) {
108  $this->internals[$ns] = $entries;
109  } else {
110  $this->internals[$ns] += $entries;
111  }
112  }
113  $this->interwikis += $other->interwikis;
114  }
115 
128  public function mergeForeign( $other, $texts ) {
129  $this->tempIdOffset = $idOffset = $this->parent->nextLinkID();
130  $maxId = 0;
131 
132  # Renumber internal links
133  foreach ( $other->internals as $ns => $nsLinks ) {
134  foreach ( $nsLinks as $key => $entry ) {
135  $newKey = $idOffset + $key;
136  $this->internals[$ns][$newKey] = $entry;
137  $maxId = $newKey > $maxId ? $newKey : $maxId;
138  }
139  }
140  $texts = preg_replace_callback( '/(<!--LINK\'" \d+:)(\d+)(-->)/',
141  [ $this, 'mergeForeignCallback' ], $texts );
142 
143  # Renumber interwiki links
144  foreach ( $other->interwikis as $key => $entry ) {
145  $newKey = $idOffset + $key;
146  $this->interwikis[$newKey] = $entry;
147  $maxId = $newKey > $maxId ? $newKey : $maxId;
148  }
149  $texts = preg_replace_callback( '/(<!--IWLINK\'" )(\d+)(-->)/',
150  [ $this, 'mergeForeignCallback' ], $texts );
151 
152  # Set the parent link ID to be beyond the highest used ID
153  $this->parent->setLinkID( $maxId + 1 );
154  $this->tempIdOffset = null;
155  return $texts;
156  }
157 
162  protected function mergeForeignCallback( $m ) {
163  return $m[1] . ( $m[2] + $this->tempIdOffset ) . $m[3];
164  }
165 
172  public function getSubArray( $text ) {
173  $sub = new LinkHolderArray( $this->parent );
174 
175  # Internal links
176  $pos = 0;
177  while ( $pos < strlen( $text ) ) {
178  if ( !preg_match( '/<!--LINK\'" (\d+):(\d+)-->/',
179  $text, $m, PREG_OFFSET_CAPTURE, $pos )
180  ) {
181  break;
182  }
183  $ns = $m[1][0];
184  $key = $m[2][0];
185  $sub->internals[$ns][$key] = $this->internals[$ns][$key];
186  $pos = $m[0][1] + strlen( $m[0][0] );
187  }
188 
189  # Interwiki links
190  $pos = 0;
191  while ( $pos < strlen( $text ) ) {
192  if ( !preg_match( '/<!--IWLINK\'" (\d+)-->/', $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
193  break;
194  }
195  $key = $m[1][0];
196  $sub->interwikis[$key] = $this->interwikis[$key];
197  $pos = $m[0][1] + strlen( $m[0][0] );
198  }
199  return $sub;
200  }
201 
206  public function isBig() {
207  global $wgLinkHolderBatchSize;
208  return $this->size > $wgLinkHolderBatchSize;
209  }
210 
215  public function clear() {
216  $this->internals = [];
217  $this->interwikis = [];
218  $this->size = 0;
219  }
220 
234  public function makeHolder( $nt, $text = '', $query = [], $trail = '', $prefix = '' ) {
235  if ( !is_object( $nt ) ) {
236  # Fail gracefully
237  $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
238  } else {
239  # Separate the link trail from the rest of the link
240  list( $inside, $trail ) = Linker::splitTrail( $trail );
241 
242  $entry = [
243  'title' => $nt,
244  'text' => $prefix . $text . $inside,
245  'pdbk' => $nt->getPrefixedDBkey(),
246  ];
247  if ( $query !== [] ) {
248  $entry['query'] = $query;
249  }
250 
251  if ( $nt->isExternal() ) {
252  // Use a globally unique ID to keep the objects mergable
253  $key = $this->parent->nextLinkID();
254  $this->interwikis[$key] = $entry;
255  $retVal = "<!--IWLINK'\" $key-->{$trail}";
256  } else {
257  $key = $this->parent->nextLinkID();
258  $ns = $nt->getNamespace();
259  $this->internals[$ns][$key] = $entry;
260  $retVal = "<!--LINK'\" $ns:$key-->{$trail}";
261  }
262  $this->size++;
263  }
264  return $retVal;
265  }
266 
272  public function replace( &$text ) {
273  $this->replaceInternal( $text );
274  $this->replaceInterwiki( $text );
275  }
276 
282  protected function replaceInternal( &$text ) {
283  if ( !$this->internals ) {
284  return;
285  }
286 
287  $colours = [];
288  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
289  $output = $this->parent->getOutput();
290  $linkRenderer = $this->parent->getLinkRenderer();
291 
292  $dbr = wfGetDB( DB_REPLICA );
293 
294  # Sort by namespace
295  ksort( $this->internals );
296 
297  $linkcolour_ids = [];
298 
299  # Generate query
300  $lb = new LinkBatch();
301  $lb->setCaller( __METHOD__ );
302 
303  foreach ( $this->internals as $ns => $entries ) {
304  foreach ( $entries as $entry ) {
306  $title = $entry['title'];
307  $pdbk = $entry['pdbk'];
308 
309  # Skip invalid entries.
310  # Result will be ugly, but prevents crash.
311  if ( is_null( $title ) ) {
312  continue;
313  }
314 
315  # Check if it's a static known link, e.g. interwiki
316  if ( $title->isAlwaysKnown() ) {
317  $colours[$pdbk] = '';
318  } elseif ( $ns == NS_SPECIAL ) {
319  $colours[$pdbk] = 'new';
320  } else {
321  $id = $linkCache->getGoodLinkID( $pdbk );
322  if ( $id != 0 ) {
323  $colours[$pdbk] = $linkRenderer->getLinkClasses( $title );
324  $output->addLink( $title, $id );
325  $linkcolour_ids[$id] = $pdbk;
326  } elseif ( $linkCache->isBadLink( $pdbk ) ) {
327  $colours[$pdbk] = 'new';
328  } else {
329  # Not in the link cache, add it to the query
330  $lb->addObj( $title );
331  }
332  }
333  }
334  }
335  if ( !$lb->isEmpty() ) {
336  $fields = array_merge(
338  [ 'page_namespace', 'page_title' ]
339  );
340 
341  $res = $dbr->select(
342  'page',
343  $fields,
344  $lb->constructSet( 'page', $dbr ),
345  __METHOD__
346  );
347 
348  # Fetch data and form into an associative array
349  # non-existent = broken
350  foreach ( $res as $s ) {
351  $title = Title::makeTitle( $s->page_namespace, $s->page_title );
352  $pdbk = $title->getPrefixedDBkey();
353  $linkCache->addGoodLinkObjFromRow( $title, $s );
354  $output->addLink( $title, $s->page_id );
355  $colours[$pdbk] = $linkRenderer->getLinkClasses( $title );
356  // add id to the extension todolist
357  $linkcolour_ids[$s->page_id] = $pdbk;
358  }
359  unset( $res );
360  }
361  if ( count( $linkcolour_ids ) ) {
362  // pass an array of page_ids to an extension
363  Hooks::run( 'GetLinkColours', [ $linkcolour_ids, &$colours, $this->parent->getTitle() ] );
364  }
365 
366  # Do a second query for different language variants of links and categories
367  if ( $this->parent->getContentLanguage()->hasVariants() ) {
368  $this->doVariants( $colours );
369  }
370 
371  # Construct search and replace arrays
372  $replacePairs = [];
373  foreach ( $this->internals as $ns => $entries ) {
374  foreach ( $entries as $index => $entry ) {
375  $pdbk = $entry['pdbk'];
376  $title = $entry['title'];
377  $query = $entry['query'] ?? [];
378  $key = "$ns:$index";
379  $searchkey = "<!--LINK'\" $key-->";
380  $displayText = $entry['text'];
381  if ( isset( $entry['selflink'] ) ) {
382  $replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayText, $query );
383  continue;
384  }
385  if ( $displayText === '' ) {
386  $displayText = null;
387  } else {
388  $displayText = new HtmlArmor( $displayText );
389  }
390  if ( !isset( $colours[$pdbk] ) ) {
391  $colours[$pdbk] = 'new';
392  }
393  $attribs = [];
394  if ( $colours[$pdbk] == 'new' ) {
395  $linkCache->addBadLinkObj( $title );
396  $output->addLink( $title, 0 );
397  $link = $linkRenderer->makeBrokenLink(
398  $title, $displayText, $attribs, $query
399  );
400  } else {
401  $link = $linkRenderer->makePreloadedLink(
402  $title, $displayText, $colours[$pdbk], $attribs, $query
403  );
404  }
405 
406  $replacePairs[$searchkey] = $link;
407  }
408  }
409 
410  # Do the thing
411  $text = preg_replace_callback(
412  '/(<!--LINK\'" .*?-->)/',
413  function ( array $matches ) use ( $replacePairs ) {
414  return $replacePairs[$matches[1]];
415  },
416  $text
417  );
418  }
419 
425  protected function replaceInterwiki( &$text ) {
426  if ( empty( $this->interwikis ) ) {
427  return;
428  }
429 
430  # Make interwiki link HTML
431  $output = $this->parent->getOutput();
432  $replacePairs = [];
433  $linkRenderer = $this->parent->getLinkRenderer();
434  foreach ( $this->interwikis as $key => $link ) {
435  $replacePairs[$key] = $linkRenderer->makeLink(
436  $link['title'],
437  new HtmlArmor( $link['text'] )
438  );
439  $output->addInterwikiLink( $link['title'] );
440  }
441 
442  $text = preg_replace_callback(
443  '/<!--IWLINK\'" (.*?)-->/',
444  function ( array $matches ) use ( $replacePairs ) {
445  return $replacePairs[$matches[1]];
446  },
447  $text
448  );
449  }
450 
455  protected function doVariants( &$colours ) {
456  $linkBatch = new LinkBatch();
457  $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
458  $output = $this->parent->getOutput();
459  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
460  $titlesToBeConverted = '';
461  $titlesAttrs = [];
462 
463  // Concatenate titles to a single string, thus we only need auto convert the
464  // single string to all variants. This would improve parser's performance
465  // significantly.
466  foreach ( $this->internals as $ns => $entries ) {
467  if ( $ns == NS_SPECIAL ) {
468  continue;
469  }
470  foreach ( $entries as $index => $entry ) {
471  $pdbk = $entry['pdbk'];
472  // we only deal with new links (in its first query)
473  if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
474  $titlesAttrs[] = [ $index, $entry['title'] ];
475  // separate titles with \0 because it would never appears
476  // in a valid title
477  $titlesToBeConverted .= $entry['title']->getText() . "\0";
478  }
479  }
480  }
481 
482  // Now do the conversion and explode string to text of titles
483  $titlesAllVariants = $this->parent->getContentLanguage()->
484  autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
485  $allVariantsName = array_keys( $titlesAllVariants );
486  foreach ( $titlesAllVariants as &$titlesVariant ) {
487  $titlesVariant = explode( "\0", $titlesVariant );
488  }
489 
490  // Then add variants of links to link batch
491  $parentTitle = $this->parent->getTitle();
492  foreach ( $titlesAttrs as $i => $attrs ) {
494  list( $index, $title ) = $attrs;
495  $ns = $title->getNamespace();
496  $text = $title->getText();
497 
498  foreach ( $allVariantsName as $variantName ) {
499  $textVariant = $titlesAllVariants[$variantName][$i];
500  if ( $textVariant === $text ) {
501  continue;
502  }
503 
504  $variantTitle = Title::makeTitle( $ns, $textVariant );
505 
506  // Self-link checking for mixed/different variant titles. At this point, we
507  // already know the exact title does not exist, so the link cannot be to a
508  // variant of the current title that exists as a separate page.
509  if ( $variantTitle->equals( $parentTitle ) && !$title->hasFragment() ) {
510  $this->internals[$ns][$index]['selflink'] = true;
511  continue 2;
512  }
513 
514  $linkBatch->addObj( $variantTitle );
515  $variantMap[$variantTitle->getPrefixedDBkey()][] = "$ns:$index";
516  }
517  }
518 
519  // process categories, check if a category exists in some variant
520  $categoryMap = []; // maps $category_variant => $category (dbkeys)
521  $varCategories = []; // category replacements oldDBkey => newDBkey
522  foreach ( $output->getCategoryLinks() as $category ) {
523  $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
524  $linkBatch->addObj( $categoryTitle );
525  $variants = $this->parent->getContentLanguage()->autoConvertToAllVariants( $category );
526  foreach ( $variants as $variant ) {
527  if ( $variant !== $category ) {
528  $variantTitle = Title::makeTitleSafe( NS_CATEGORY, $variant );
529  if ( is_null( $variantTitle ) ) {
530  continue;
531  }
532  $linkBatch->addObj( $variantTitle );
533  $categoryMap[$variant] = [ $category, $categoryTitle ];
534  }
535  }
536  }
537 
538  if ( !$linkBatch->isEmpty() ) {
539  // construct query
540  $dbr = wfGetDB( DB_REPLICA );
541  $fields = array_merge(
543  [ 'page_namespace', 'page_title' ]
544  );
545 
546  $varRes = $dbr->select( 'page',
547  $fields,
548  $linkBatch->constructSet( 'page', $dbr ),
549  __METHOD__
550  );
551 
552  $linkcolour_ids = [];
553  $linkRenderer = $this->parent->getLinkRenderer();
554 
555  // for each found variants, figure out link holders and replace
556  foreach ( $varRes as $s ) {
557  $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
558  $varPdbk = $variantTitle->getPrefixedDBkey();
559  $vardbk = $variantTitle->getDBkey();
560 
561  $holderKeys = [];
562  if ( isset( $variantMap[$varPdbk] ) ) {
563  $holderKeys = $variantMap[$varPdbk];
564  $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
565  $output->addLink( $variantTitle, $s->page_id );
566  }
567 
568  // loop over link holders
569  foreach ( $holderKeys as $key ) {
570  list( $ns, $index ) = explode( ':', $key, 2 );
571  $entry =& $this->internals[$ns][$index];
572  $pdbk = $entry['pdbk'];
573 
574  if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
575  // found link in some of the variants, replace the link holder data
576  $entry['title'] = $variantTitle;
577  $entry['pdbk'] = $varPdbk;
578 
579  // set pdbk and colour
580  $colours[$varPdbk] = $linkRenderer->getLinkClasses( $variantTitle );
581  $linkcolour_ids[$s->page_id] = $pdbk;
582  }
583  }
584 
585  // check if the object is a variant of a category
586  if ( isset( $categoryMap[$vardbk] ) ) {
587  list( $oldkey, $oldtitle ) = $categoryMap[$vardbk];
588  if ( !isset( $varCategories[$oldkey] ) && !$oldtitle->exists() ) {
589  $varCategories[$oldkey] = $vardbk;
590  }
591  }
592  }
593  Hooks::run( 'GetLinkColours', [ $linkcolour_ids, &$colours, $this->parent->getTitle() ] );
594 
595  // rebuild the categories in original order (if there are replacements)
596  if ( count( $varCategories ) > 0 ) {
597  $newCats = [];
598  $originalCats = $output->getCategories();
599  foreach ( $originalCats as $cat => $sortkey ) {
600  // make the replacement
601  if ( array_key_exists( $cat, $varCategories ) ) {
602  $newCats[$varCategories[$cat]] = $sortkey;
603  } else {
604  $newCats[$cat] = $sortkey;
605  }
606  }
607  $output->setCategoryLinks( $newCats );
608  }
609  }
610  }
611 
619  public function replaceText( $text ) {
620  $text = preg_replace_callback(
621  '/<!--(LINK|IWLINK)\'" (.*?)-->/',
622  [ $this, 'replaceTextCallback' ],
623  $text );
624 
625  return $text;
626  }
627 
635  public function replaceTextCallback( $matches ) {
636  list( , $type, $key ) = $matches;
637  if ( $type == 'LINK' ) {
638  list( $ns, $index ) = explode( ':', $key, 2 );
639  if ( isset( $this->internals[$ns][$index]['text'] ) ) {
640  return $this->internals[$ns][$index]['text'];
641  }
642  } elseif ( $type == 'IWLINK' ) {
643  if ( isset( $this->interwikis[$key]['text'] ) ) {
644  return $this->interwikis[$key]['text'];
645  }
646  }
647  return $matches[0];
648  }
649 }
LinkHolderArray\replaceInterwiki
replaceInterwiki(&$text)
Replace interwiki links.
Definition: LinkHolderArray.php:425
LinkHolderArray\isBig
isBig()
Returns true if the memory requirements of this object are getting large.
Definition: LinkHolderArray.php:206
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:316
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:34
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
Linker\makeSelfLinkObj
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:163
LinkHolderArray\clear
clear()
Clear all stored link holders.
Definition: LinkHolderArray.php:215
LinkHolderArray\$size
$size
Definition: LinkHolderArray.php:32
LinkHolderArray\merge
merge( $other)
Merge another LinkHolderArray into this one.
Definition: LinkHolderArray.php:104
LinkHolderArray\__wakeup
__wakeup()
Recreate the Title objects.
Definition: LinkHolderArray.php:85
$s
$s
Definition: mergeMessageFileList.php:185
LinkHolderArray\replaceText
replaceText( $text)
Replace link placeholders with plain text of links (not HTML-formatted).
Definition: LinkHolderArray.php:619
$res
$res
Definition: testCompression.php:52
LinkHolderArray\$interwikis
$interwikis
Definition: LinkHolderArray.php:31
LinkHolderArray\replace
replace(&$text)
Replace link placeholders with actual links, in the buffer.
Definition: LinkHolderArray.php:272
LinkCache\getSelectFields
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:219
$dbr
$dbr
Definition: testCompression.php:50
LinkHolderArray\replaceTextCallback
replaceTextCallback( $matches)
Callback for replaceText()
Definition: LinkHolderArray.php:635
LinkHolderArray\__destruct
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: LinkHolderArray.php:50
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:49
LinkHolderArray\__sleep
__sleep()
Don't serialize the parent object, it is big, and not needed when it is a parameter to mergeForeign()...
Definition: LinkHolderArray.php:65
$wgLinkHolderBatchSize
$wgLinkHolderBatchSize
LinkHolderArray batch size For debugging.
Definition: DefaultSettings.php:8539
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
$matches
$matches
Definition: NoLocalSettings.php:24
LinkHolderArray
Definition: LinkHolderArray.php:29
$title
$title
Definition: testCompression.php:34
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
$output
$output
Definition: SyntaxHighlight.php:335
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:282
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:1775
LinkHolderArray\doVariants
doVariants(&$colours)
Modify $this->internals and $colours according to language variant linking rules.
Definition: LinkHolderArray.php:455
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:613
LinkHolderArray\$internals
$internals
Definition: LinkHolderArray.php:30
LinkHolderArray\$tempIdOffset
$tempIdOffset
Definition: LinkHolderArray.php:38
Title
Represents a title within MediaWiki.
Definition: Title.php:42
LinkHolderArray\mergeForeign
mergeForeign( $other, $texts)
Merge a LinkHolderArray from another parser instance into this one.
Definition: LinkHolderArray.php:128
LinkHolderArray\mergeForeignCallback
mergeForeignCallback( $m)
Definition: LinkHolderArray.php:162
LinkHolderArray\$parent
Parser $parent
Definition: LinkHolderArray.php:37
LinkHolderArray\__construct
__construct( $parent)
Definition: LinkHolderArray.php:43
LinkHolderArray\makeHolder
makeHolder( $nt, $text='', $query=[], $trail='', $prefix='')
Make a link placeholder.
Definition: LinkHolderArray.php:234
LinkHolderArray\getSubArray
getSubArray( $text)
Get a subset of the current LinkHolderArray which is sufficient to interpret the given text.
Definition: LinkHolderArray.php:172
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
$type
$type
Definition: testCompression.php:48