MediaWiki  master
Linker.php
Go to the documentation of this file.
1 <?php
23 use HtmlFormatter\HtmlFormatter;
29 use Wikimedia\IPUtils;
30 
40 class Linker {
44  public const TOOL_LINKS_NOBLOCK = 1;
45  public const TOOL_LINKS_EMAIL = 2;
46 
89  public static function link(
90  $target, $html = null, $customAttribs = [], $query = [], $options = []
91  ) {
92  if ( !$target instanceof LinkTarget ) {
93  wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
94  return "<!-- ERROR -->$html";
95  }
96 
97  $services = MediaWikiServices::getInstance();
98  $options = (array)$options;
99  if ( $options ) {
100  // Custom options, create new LinkRenderer
101  $linkRenderer = $services->getLinkRendererFactory()
102  ->createFromLegacyOptions( $options );
103  } else {
104  $linkRenderer = $services->getLinkRenderer();
105  }
106 
107  if ( $html !== null ) {
108  $text = new HtmlArmor( $html );
109  } else {
110  $text = null;
111  }
112 
113  if ( in_array( 'known', $options, true ) ) {
114  return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
115  }
116 
117  if ( in_array( 'broken', $options, true ) ) {
118  return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
119  }
120 
121  if ( in_array( 'noclasses', $options, true ) ) {
122  return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query );
123  }
124 
125  return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
126  }
127 
141  public static function linkKnown(
142  $target, $html = null, $customAttribs = [],
143  $query = [], $options = [ 'known' ]
144  ) {
145  return self::link( $target, $html, $customAttribs, $query, $options );
146  }
147 
163  public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
164  $nt = Title::newFromLinkTarget( $nt );
165  $ret = "<a class=\"mw-selflink selflink\">{$prefix}{$html}</a>{$trail}";
166  if ( !Hooks::runner()->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
167  return $ret;
168  }
169 
170  if ( $html == '' ) {
171  $html = htmlspecialchars( $nt->getPrefixedText() );
172  }
173  [ $inside, $trail ] = self::splitTrail( $trail );
174  return "<a class=\"mw-selflink selflink\">{$prefix}{$html}{$inside}</a>{$trail}";
175  }
176 
187  public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
188  // First we check whether the namespace exists or not.
189  if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
190  if ( $namespace == NS_MAIN ) {
191  $name = $context->msg( 'blanknamespace' )->text();
192  } else {
193  $name = MediaWikiServices::getInstance()->getContentLanguage()->
194  getFormattedNsText( $namespace );
195  }
196  return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
197  }
198 
199  return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
200  }
201 
208  public static function normaliseSpecialPage( LinkTarget $target ) {
209  wfDeprecated( __METHOD__, '1.35' );
210  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
211  return $linkRenderer->normalizeTarget( $target );
212  }
213 
222  private static function fnamePart( $url ) {
223  $basename = strrchr( $url, '/' );
224  if ( $basename === false ) {
225  $basename = $url;
226  } else {
227  $basename = substr( $basename, 1 );
228  }
229  return $basename;
230  }
231 
242  public static function makeExternalImage( $url, $alt = '' ) {
243  if ( $alt == '' ) {
244  $alt = self::fnamePart( $url );
245  }
246  $img = '';
247  $success = Hooks::runner()->onLinkerMakeExternalImage( $url, $alt, $img );
248  if ( !$success ) {
249  wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
250  . "with url {$url} and alt text {$alt} to {$img}" );
251  return $img;
252  }
253  return Html::element( 'img',
254  [
255  'src' => $url,
256  'alt' => $alt
257  ]
258  );
259  }
260 
299  public static function makeImageLink( Parser $parser, LinkTarget $title,
300  $file, $frameParams = [], $handlerParams = [], $time = false,
301  $query = "", $widthOption = null
302  ) {
304  $res = null;
305  $dummy = new DummyLinker;
306  if ( !Hooks::runner()->onImageBeforeProduceHTML( $dummy, $title,
307  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
308  $file, $frameParams, $handlerParams, $time, $res,
309  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
310  $parser, $query, $widthOption )
311  ) {
312  return $res;
313  }
314 
315  if ( $file && !$file->allowInlineDisplay() ) {
316  wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display" );
317  return self::link( $title );
318  }
319 
320  // Clean up parameters
321  $page = $handlerParams['page'] ?? false;
322  if ( !isset( $frameParams['align'] ) ) {
323  $frameParams['align'] = '';
324  }
325  if ( !isset( $frameParams['alt'] ) ) {
326  $frameParams['alt'] = '';
327  }
328  if ( !isset( $frameParams['title'] ) ) {
329  $frameParams['title'] = '';
330  }
331  if ( !isset( $frameParams['class'] ) ) {
332  $frameParams['class'] = '';
333  }
334 
335  $services = MediaWikiServices::getInstance();
336  $config = $services->getMainConfig();
337  $enableLegacyMediaDOM = $config->get( MainConfigNames::ParserEnableLegacyMediaDOM );
338 
339  $classes = [];
340  if (
341  !isset( $handlerParams['width'] ) &&
342  !isset( $frameParams['manualthumb'] ) &&
343  !isset( $frameParams['framed'] )
344  ) {
345  $classes[] = 'mw-default-size';
346  }
347 
348  $prefix = $postfix = '';
349 
350  if ( $enableLegacyMediaDOM ) {
351  if ( $frameParams['align'] == 'center' ) {
352  $prefix = '<div class="center">';
353  $postfix = '</div>';
354  $frameParams['align'] = 'none';
355  }
356  }
357 
358  if ( $file && !isset( $handlerParams['width'] ) ) {
359  if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
360  // If its a vector image, and user only specifies height
361  // we don't want it to be limited by its "normal" width.
362  $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
363  $handlerParams['width'] = $svgMaxSize;
364  } else {
365  $handlerParams['width'] = $file->getWidth( $page );
366  }
367 
368  if ( isset( $frameParams['thumbnail'] )
369  || isset( $frameParams['manualthumb'] )
370  || isset( $frameParams['framed'] )
371  || isset( $frameParams['frameless'] )
372  || !$handlerParams['width']
373  ) {
374  $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
375  $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
376  if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
377  $userOptionsLookup = $services->getUserOptionsLookup();
378  $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
379  }
380 
381  // Reduce width for upright images when parameter 'upright' is used
382  if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
383  $frameParams['upright'] = $thumbUpright;
384  }
385 
386  // For caching health: If width scaled down due to upright
387  // parameter, round to full __0 pixel to avoid the creation of a
388  // lot of odd thumbs.
389  $prefWidth = isset( $frameParams['upright'] ) ?
390  round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
391  $thumbLimits[$widthOption];
392 
393  // Use width which is smaller: real image width or user preference width
394  // Unless image is scalable vector.
395  if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
396  $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
397  $handlerParams['width'] = $prefWidth;
398  }
399  }
400  }
401 
402  // Parser::makeImage has a similarly named variable
403  $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
404  isset( $frameParams['manualthumb'] ) ||
405  isset( $frameParams['framed'] );
406 
407  if ( $hasVisibleCaption ) {
408  if ( $enableLegacyMediaDOM ) {
409  // This is no longer needed in our new media output, since the
410  // default styling in content.media-common.less takes care of it;
411  // see T269704.
412 
413  # Create a thumbnail. Alignment depends on the writing direction of
414  # the page content language (right-aligned for LTR languages,
415  # left-aligned for RTL languages)
416  # If a thumbnail width has not been provided, it is set
417  # to the default user option as specified in Language*.php
418  if ( $frameParams['align'] == '' ) {
419  $frameParams['align'] = $parser->getTargetLanguage()->alignEnd();
420  }
421  }
422  return $prefix . self::makeThumbLink2(
423  $title, $file, $frameParams, $handlerParams, $time, $query,
424  $classes, $parser
425  ) . $postfix;
426  }
427 
428  $rdfaType = 'mw:File';
429 
430  if ( $file && isset( $frameParams['frameless'] ) ) {
431  $rdfaType .= '/Frameless';
432  $srcWidth = $file->getWidth( $page );
433  # For "frameless" option: do not present an image bigger than the
434  # source (for bitmap-style images). This is the same behavior as the
435  # "thumb" option does it already.
436  if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
437  $handlerParams['width'] = $srcWidth;
438  }
439  }
440 
441  if ( $file && isset( $handlerParams['width'] ) ) {
442  # Create a resized image, without the additional thumbnail features
443  $thumb = $file->transform( $handlerParams );
444  } else {
445  $thumb = false;
446  }
447 
448  if ( !$thumb ) {
449  $rdfaType = 'mw:Error ' . $rdfaType;
450  $label = '';
451  if ( $enableLegacyMediaDOM ) {
452  // This is the information for tooltips for inline images which
453  // Parsoid stores in data-mw. See T273014
454  $label = $frameParams['title'];
455  }
457  $title, $label, '', '', '', (bool)$time, $handlerParams
458  );
459  } else {
460  self::processResponsiveImages( $file, $thumb, $handlerParams );
461  $params = [
462  'alt' => $frameParams['alt'],
463  'title' => $frameParams['title'],
464  ];
465  if ( $enableLegacyMediaDOM ) {
466  $params += [
467  'valign' => $frameParams['valign'] ?? false,
468  'img-class' => $frameParams['class'],
469  ];
470  if ( isset( $frameParams['border'] ) ) {
471  $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
472  }
473  }
474  $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
475  $s = $thumb->toHtml( $params );
476  }
477 
478  if ( $enableLegacyMediaDOM ) {
479  if ( $frameParams['align'] != '' ) {
481  'div',
482  [ 'class' => 'float' . $frameParams['align'] ],
483  $s
484  );
485  }
486  return str_replace( "\n", ' ', $prefix . $s . $postfix );
487  }
488 
489  $wrapper = 'span';
490  $caption = '';
491 
492  if ( $frameParams['align'] != '' ) {
493  $wrapper = 'figure';
494  // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
495  $classes[] = "mw-halign-{$frameParams['align']}";
496  $caption = Html::rawElement(
497  'figcaption', [], $frameParams['caption'] ?? ''
498  );
499  } elseif ( isset( $frameParams['valign'] ) ) {
500  // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
501  // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
502  // mw-valign-text-bottom
503  $classes[] = "mw-valign-{$frameParams['valign']}";
504  }
505 
506  if ( isset( $frameParams['border'] ) ) {
507  $classes[] = 'mw-image-border';
508  }
509 
510  if ( isset( $frameParams['class'] ) ) {
511  $classes[] = $frameParams['class'];
512  }
513 
514  $attribs = [
515  'class' => $classes,
516  'typeof' => $rdfaType,
517  ];
518 
519  $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
520 
521  return str_replace( "\n", ' ', $s );
522  }
523 
532  public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
533  $mtoParams = [];
534  if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
535  $mtoParams['custom-url-link'] = $frameParams['link-url'];
536  if ( isset( $frameParams['link-target'] ) ) {
537  $mtoParams['custom-target-link'] = $frameParams['link-target'];
538  }
539  if ( $parser ) {
540  $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
541  foreach ( $extLinkAttrs as $name => $val ) {
542  // Currently could include 'rel' and 'target'
543  $mtoParams['parser-extlink-' . $name] = $val;
544  }
545  }
546  } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
547  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
548  $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
549  $linkRenderer->normalizeTarget( $frameParams['link-title'] )
550  );
551  if ( isset( $frameParams['link-title-query'] ) ) {
552  $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
553  }
554  } elseif ( !empty( $frameParams['no-link'] ) ) {
555  // No link
556  } else {
557  $mtoParams['desc-link'] = true;
558  $mtoParams['desc-query'] = $query;
559  }
560  return $mtoParams;
561  }
562 
575  public static function makeThumbLinkObj(
576  LinkTarget $title, $file, $label = '', $alt = '', $align = null,
577  $params = [], $framed = false, $manualthumb = ""
578  ) {
579  $frameParams = [
580  'alt' => $alt,
581  'caption' => $label,
582  'align' => $align
583  ];
584  $classes = [];
585  if ( $manualthumb ) {
586  $frameParams['manualthumb'] = $manualthumb;
587  } elseif ( $framed ) {
588  $frameParams['framed'] = true;
589  } elseif ( !isset( $params['width'] ) ) {
590  $classes[] = 'mw-default-size';
591  }
592  return self::makeThumbLink2(
593  $title, $file, $frameParams, $params, false, '', $classes
594  );
595  }
596 
608  public static function makeThumbLink2(
609  LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
610  $time = false, $query = "", array $classes = [], ?Parser $parser = null
611  ) {
612  $exists = $file && $file->exists();
613 
614  $services = MediaWikiServices::getInstance();
615  $enableLegacyMediaDOM = $services->getMainConfig()->get( MainConfigNames::ParserEnableLegacyMediaDOM );
616 
617  $page = $handlerParams['page'] ?? false;
618  if ( !isset( $frameParams['align'] ) ) {
619  $frameParams['align'] = '';
620  if ( $enableLegacyMediaDOM ) {
621  $frameParams['align'] = 'right';
622  }
623  }
624  if ( !isset( $frameParams['alt'] ) ) {
625  $frameParams['alt'] = '';
626  }
627  if ( !isset( $frameParams['caption'] ) ) {
628  $frameParams['caption'] = '';
629  }
630 
631  if ( empty( $handlerParams['width'] ) ) {
632  // Reduce width for upright images when parameter 'upright' is used
633  $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
634  }
635 
636  $thumb = false;
637  $noscale = false;
638  $manualthumb = false;
639  $rdfaType = 'mw:File/Thumb';
640 
641  if ( !$exists ) {
642  $outerWidth = $handlerParams['width'] + 2;
643  } else {
644  if ( isset( $frameParams['manualthumb'] ) ) {
645  # Use manually specified thumbnail
646  $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
647  if ( $manual_title ) {
648  $manual_img = $services->getRepoGroup()
649  ->findFile( $manual_title );
650  if ( $manual_img ) {
651  $thumb = $manual_img->getUnscaledThumb( $handlerParams );
652  $manualthumb = true;
653  } else {
654  $exists = false;
655  }
656  }
657  } elseif ( isset( $frameParams['framed'] ) ) {
658  // Use image dimensions, don't scale
659  $thumb = $file->getUnscaledThumb( $handlerParams );
660  $noscale = true;
661  $rdfaType = 'mw:File/Frame';
662  } else {
663  # Do not present an image bigger than the source, for bitmap-style images
664  # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
665  $srcWidth = $file->getWidth( $page );
666  if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
667  $handlerParams['width'] = $srcWidth;
668  }
669  $thumb = $file->transform( $handlerParams );
670  }
671 
672  if ( $thumb ) {
673  $outerWidth = $thumb->getWidth() + 2;
674  } else {
675  $outerWidth = $handlerParams['width'] + 2;
676  }
677  }
678 
679  $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
680  $linkTitleQuery = [];
681 
682  if ( $page ) {
683  $linkTitleQuery['page'] = $page;
684  # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
685  # So we don't need to pass it here in $query. However, the URL for the
686  # zoom icon still needs it, so we make a unique query for it. See T16771
687  # FIXME: What about "lang" and other querystring parameters
688  $url = wfAppendQuery( $url, $linkTitleQuery );
689  }
690 
691  if ( $manualthumb
692  && !isset( $frameParams['link-title'] )
693  && !isset( $frameParams['link-url'] )
694  && !isset( $frameParams['no-link'] ) ) {
695  $frameParams['link-title'] = $title;
696  $frameParams['link-title-query'] = $linkTitleQuery;
697  }
698 
699  if ( $frameParams['align'] != '' ) {
700  // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
701  $classes[] = "mw-halign-{$frameParams['align']}";
702  }
703 
704  if ( isset( $frameParams['class'] ) ) {
705  $classes[] = $frameParams['class'];
706  }
707 
708  $s = '';
709 
710  if ( $enableLegacyMediaDOM ) {
711  $s .= "<div class=\"thumb t{$frameParams['align']}\">"
712  . "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
713  }
714 
715  if ( !$exists ) {
716  $label = '';
718  $title, $label, '', '', '', (bool)$time, $handlerParams
719  );
720  $zoomIcon = '';
721  } elseif ( !$thumb ) {
722  if ( $enableLegacyMediaDOM ) {
723  $s .= wfMessage( 'thumbnail_error', '' )->escaped();
724  } else {
726  $title, '', '', '', '', (bool)$time, $handlerParams
727  );
728  }
729  $zoomIcon = '';
730  } else {
731  if ( !$noscale && !$manualthumb ) {
732  self::processResponsiveImages( $file, $thumb, $handlerParams );
733  }
734  $params = [
735  'alt' => $frameParams['alt'],
736  ];
737  if ( $enableLegacyMediaDOM ) {
738  $params += [
739  'img-class' => ( isset( $frameParams['class'] ) && $frameParams['class'] !== ''
740  ? $frameParams['class'] . ' '
741  : '' ) . 'thumbimage'
742  ];
743  }
744  $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
745  $s .= $thumb->toHtml( $params );
746  if ( isset( $frameParams['framed'] ) ) {
747  $zoomIcon = "";
748  } else {
749  $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
750  Html::rawElement( 'a', [
751  'href' => $url,
752  'class' => 'internal',
753  'title' => wfMessage( 'thumbnail-more' )->text() ],
754  "" ) );
755  }
756  }
757 
758  if ( $enableLegacyMediaDOM ) {
759  $s .= ' <div class="thumbcaption">' . $zoomIcon . $frameParams['caption'] . "</div></div></div>";
760  return str_replace( "\n", ' ', $s );
761  }
762 
763  $s .= Html::rawElement(
764  'figcaption', [], $frameParams['caption'] ?? ''
765  );
766 
767  if ( !$exists || !$thumb ) {
768  $rdfaType = 'mw:Error ' . $rdfaType;
769  }
770 
771  $attribs = [
772  'class' => $classes,
773  'typeof' => $rdfaType,
774  ];
775 
776  $s = Html::rawElement( 'figure', $attribs, $s );
777 
778  return str_replace( "\n", ' ', $s );
779  }
780 
789  public static function processResponsiveImages( $file, $thumb, $hp ) {
790  $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
791  if ( $responsiveImages && $thumb && !$thumb->isError() ) {
792  $hp15 = $hp;
793  $hp15['width'] = round( $hp['width'] * 1.5 );
794  $hp20 = $hp;
795  $hp20['width'] = $hp['width'] * 2;
796  if ( isset( $hp['height'] ) ) {
797  $hp15['height'] = round( $hp['height'] * 1.5 );
798  $hp20['height'] = $hp['height'] * 2;
799  }
800 
801  $thumb15 = $file->transform( $hp15 );
802  $thumb20 = $file->transform( $hp20 );
803  if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
804  $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
805  }
806  if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
807  $thumb->responsiveUrls['2'] = $thumb20->getUrl();
808  }
809  }
810  }
811 
825  public static function makeBrokenImageLinkObj(
826  $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
827  $time = false, array $handlerParams = []
828  ) {
829  if ( !$title instanceof LinkTarget ) {
830  wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
831  return "<!-- ERROR -->" . htmlspecialchars( $label );
832  }
833 
835  $services = MediaWikiServices::getInstance();
836  $mainConfig = $services->getMainConfig();
837  $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
838  $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
839  $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
840  if ( $label == '' ) {
841  $label = $title->getPrefixedText();
842  }
843 
844  $html = Html::element( 'span', [
845  'class' => 'mw-broken-media',
846  // These data attributes are used to dynamically size the span, see T273013
847  'data-width' => $handlerParams['width'] ?? null,
848  'data-height' => $handlerParams['height'] ?? null,
849  ], $label );
850 
851  if ( $mainConfig->get( MainConfigNames::ParserEnableLegacyMediaDOM ) ) {
852  $html = htmlspecialchars( $label, ENT_COMPAT );
853  }
854 
855  $repoGroup = $services->getRepoGroup();
856  $currentExists = $time
857  && $repoGroup->findFile( $title ) !== false;
858 
859  if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
860  && !$currentExists
861  ) {
862  if (
863  $title->inNamespace( NS_FILE ) &&
864  $repoGroup->getLocalRepo()->checkRedirect( $title )
865  ) {
866  // We already know it's a redirect, so mark it accordingly
867  return self::link(
868  $title,
869  $html,
870  [ 'class' => 'mw-redirect' ],
871  wfCgiToArray( $query ),
872  [ 'known', 'noclasses' ]
873  );
874  }
875  return Html::rawElement( 'a', [
876  'href' => self::getUploadUrl( $title, $query ),
877  'class' => 'new',
878  'title' => $title->getPrefixedText()
879  ], $html );
880  }
881  return self::link(
882  $title,
883  $html,
884  [],
885  wfCgiToArray( $query ),
886  [ 'known', 'noclasses' ]
887  );
888  }
889 
898  protected static function getUploadUrl( $destFile, $query = '' ) {
899  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
900  $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
901  $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
902  $q = 'wpDestFile=' . Title::castFromLinkTarget( $destFile )->getPartialURL();
903  if ( $query != '' ) {
904  $q .= '&' . $query;
905  }
906 
907  if ( $uploadMissingFileUrl ) {
908  return wfAppendQuery( $uploadMissingFileUrl, $q );
909  }
910 
911  if ( $uploadNavigationUrl ) {
912  return wfAppendQuery( $uploadNavigationUrl, $q );
913  }
914 
915  $upload = SpecialPage::getTitleFor( 'Upload' );
916 
917  return $upload->getLocalURL( $q );
918  }
919 
929  public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
930  $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
931  $title, [ 'time' => $time ]
932  );
933  return self::makeMediaLinkFile( $title, $img, $html );
934  }
935 
948  public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
949  if ( $file && $file->exists() ) {
950  $url = $file->getUrl();
951  $class = 'internal';
952  } else {
953  $url = self::getUploadUrl( $title );
954  $class = 'new';
955  }
956 
957  $alt = $title->getText();
958  if ( $html == '' ) {
959  $html = $alt;
960  }
961 
962  $ret = '';
963  $attribs = [
964  'href' => $url,
965  'class' => $class,
966  'title' => $alt
967  ];
968 
969  if ( !Hooks::runner()->onLinkerMakeMediaLinkFile(
970  Title::castFromLinkTarget( $title ), $file, $html, $attribs, $ret )
971  ) {
972  wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
973  . "with url {$url} and text {$html} to {$ret}" );
974  return $ret;
975  }
976 
977  return Html::rawElement( 'a', $attribs, $html );
978  }
979 
990  public static function specialLink( $name, $key = '' ) {
991  if ( $key == '' ) {
992  $key = strtolower( $name );
993  }
994 
995  return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->escaped() );
996  }
997 
1016  public static function makeExternalLink( $url, $text, $escape = true,
1017  $linktype = '', $attribs = [], $title = null
1018  ) {
1019  global $wgTitle;
1020  $class = "external";
1021  if ( $linktype ) {
1022  $class .= " $linktype";
1023  }
1024  if ( isset( $attribs['class'] ) && $attribs['class'] ) {
1025  $class .= " {$attribs['class']}";
1026  }
1027  $attribs['class'] = $class;
1028 
1029  if ( $escape ) {
1030  $text = htmlspecialchars( $text, ENT_COMPAT );
1031  }
1032 
1033  if ( !$title ) {
1034  $title = $wgTitle;
1035  }
1036  $newRel = Parser::getExternalLinkRel( $url, $title );
1037  if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) {
1038  $attribs['rel'] = $newRel;
1039  } elseif ( $newRel !== '' ) {
1040  // Merge the rel attributes.
1041  $newRels = explode( ' ', $newRel );
1042  $oldRels = explode( ' ', $attribs['rel'] );
1043  $combined = array_unique( array_merge( $newRels, $oldRels ) );
1044  $attribs['rel'] = implode( ' ', $combined );
1045  }
1046  $link = '';
1047  $success = Hooks::runner()->onLinkerMakeExternalLink(
1048  $url, $text, $link, $attribs, $linktype );
1049  if ( !$success ) {
1050  wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
1051  . "with url {$url} and text {$text} to {$link}" );
1052  return $link;
1053  }
1054  $attribs['href'] = $url;
1055  return Html::rawElement( 'a', $attribs, $text );
1056  }
1057 
1069  public static function userLink( $userId, $userName, $altUserName = false ) {
1070  if ( $userName === '' || $userName === false || $userName === null ) {
1071  wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1072  'that need to be fixed?' );
1073  return wfMessage( 'empty-username' )->parse();
1074  }
1075 
1076  $classes = 'mw-userlink';
1077  $page = null;
1078  if ( $userId == 0 ) {
1079  $page = ExternalUserNames::getUserLinkTitle( $userName );
1080 
1081  if ( ExternalUserNames::isExternal( $userName ) ) {
1082  $classes .= ' mw-extuserlink';
1083  } elseif ( $altUserName === false ) {
1084  $altUserName = IPUtils::prettifyIP( $userName );
1085  }
1086  $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
1087  } else {
1088  $page = TitleValue::tryNew( NS_USER, strtr( $userName, ' ', '_' ) );
1089  }
1090 
1091  // Wrap the output with <bdi> tags for directionality isolation
1092  $linkText =
1093  '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>';
1094 
1095  return $page
1096  ? self::link( $page, $linkText, [ 'class' => $classes ] )
1097  : Html::rawElement( 'span', [ 'class' => $classes ], $linkText );
1098  }
1099 
1114  public static function userToolLinks(
1115  $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1116  $useParentheses = true
1117  ) {
1118  if ( $userText === '' ) {
1119  wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1120  'that need to be fixed?' );
1121  return ' ' . wfMessage( 'empty-username' )->parse();
1122  }
1123  global $wgLang;
1124  $disableAnonTalk = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1125  $talkable = !( $disableAnonTalk && $userId == 0 );
1126  $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1127  $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1128 
1129  if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1130  // No tools for an external user
1131  return '';
1132  }
1133 
1134  $items = [];
1135  if ( $talkable ) {
1136  $items[] = self::userTalkLink( $userId, $userText );
1137  }
1138  if ( $userId ) {
1139  // check if the user has an edit
1140  $attribs = [];
1141  $attribs['class'] = 'mw-usertoollinks-contribs';
1142  if ( $redContribsWhenNoEdits ) {
1143  if ( intval( $edits ) === 0 && $edits !== 0 ) {
1144  $user = User::newFromId( $userId );
1145  $edits = $user->getEditCount();
1146  }
1147  if ( $edits === 0 ) {
1148  $attribs['class'] .= ' new';
1149  }
1150  }
1151  $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1152 
1153  $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1154  }
1155  $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1156  if ( $blockable && $userCanBlock ) {
1157  $items[] = self::blockLink( $userId, $userText );
1158  }
1159 
1160  $user = RequestContext::getMain()->getUser();
1161  if ( $addEmailLink && $user->canSendEmail() ) {
1162  $items[] = self::emailLink( $userId, $userText );
1163  }
1164 
1165  Hooks::runner()->onUserToolLinksEdit( $userId, $userText, $items );
1166 
1167  if ( !$items ) {
1168  return '';
1169  }
1170 
1171  if ( $useParentheses ) {
1172  return wfMessage( 'word-separator' )->escaped()
1173  . '<span class="mw-usertoollinks">'
1174  . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1175  . '</span>';
1176  }
1177 
1178  $tools = [];
1179  foreach ( $items as $tool ) {
1180  $tools[] = Html::rawElement( 'span', [], $tool );
1181  }
1182  return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1183  implode( ' ', $tools ) . '</span>';
1184  }
1185 
1195  public static function userToolLinksRedContribs(
1196  $userId, $userText, $edits = null, $useParentheses = true
1197  ) {
1198  return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1199  }
1200 
1207  public static function userTalkLink( $userId, $userText ) {
1208  if ( $userText === '' ) {
1209  wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1210  'that need to be fixed?' );
1211  return wfMessage( 'empty-username' )->parse();
1212  }
1213 
1214  $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1215  $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1216  $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1217 
1218  return $userTalkPage
1219  ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1220  : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1221  }
1222 
1229  public static function blockLink( $userId, $userText ) {
1230  if ( $userText === '' ) {
1231  wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1232  'that need to be fixed?' );
1233  return wfMessage( 'empty-username' )->parse();
1234  }
1235 
1236  $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1237  $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1238 
1239  return self::link( $blockPage,
1240  wfMessage( 'blocklink' )->escaped(),
1241  $moreLinkAttribs
1242  );
1243  }
1244 
1250  public static function emailLink( $userId, $userText ) {
1251  if ( $userText === '' ) {
1252  wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1253  'that need to be fixed?' );
1254  return wfMessage( 'empty-username' )->parse();
1255  }
1256 
1257  $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1258  $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1259  return self::link( $emailPage,
1260  wfMessage( 'emaillink' )->escaped(),
1261  $moreLinkAttribs
1262  );
1263  }
1264 
1276  public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1277  // TODO inject authority
1278  $authority = RequestContext::getMain()->getAuthority();
1279 
1280  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) && $isPublic ) {
1281  $link = wfMessage( 'rev-deleted-user' )->escaped();
1282  } elseif ( $revRecord->userCan( RevisionRecord::DELETED_USER, $authority ) ) {
1283  $revUser = $revRecord->getUser( RevisionRecord::FOR_THIS_USER, $authority );
1284  $link = self::userLink(
1285  $revUser ? $revUser->getId() : 0,
1286  $revUser ? $revUser->getName() : ''
1287  );
1288  } else {
1289  $link = wfMessage( 'rev-deleted-user' )->escaped();
1290  }
1291  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1292  $class = self::getRevisionDeletedClass( $revRecord );
1293  return '<span class="' . $class . '">' . $link . '</span>';
1294  }
1295  return $link;
1296  }
1297 
1304  public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1305  $class = 'history-deleted';
1306  if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1307  $class .= ' mw-history-suppressed';
1308  }
1309  return $class;
1310  }
1311 
1324  public static function revUserTools(
1325  RevisionRecord $revRecord,
1326  $isPublic = false,
1327  $useParentheses = true
1328  ) {
1329  // TODO inject authority
1330  $authority = RequestContext::getMain()->getAuthority();
1331 
1332  if ( $revRecord->userCan( RevisionRecord::DELETED_USER, $authority ) &&
1333  ( !$revRecord->isDeleted( RevisionRecord::DELETED_USER ) || !$isPublic )
1334  ) {
1335  $revUser = $revRecord->getUser( RevisionRecord::FOR_THIS_USER, $authority );
1336  $userId = $revUser ? $revUser->getId() : 0;
1337  $userText = $revUser ? $revUser->getName() : '';
1338 
1339  if ( $userId || $userText !== '' ) {
1340  $link = self::userLink( $userId, $userText )
1341  . self::userToolLinks( $userId, $userText, false, 0, null,
1342  $useParentheses );
1343  }
1344  }
1345 
1346  if ( !isset( $link ) ) {
1347  $link = wfMessage( 'rev-deleted-user' )->escaped();
1348  }
1349 
1350  if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1351  $class = self::getRevisionDeletedClass( $revRecord );
1352  return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1353  }
1354  return $link;
1355  }
1356 
1367  public static function expandLocalLinks( string $html ) {
1368  $formatter = new HtmlFormatter( $html );
1369  $doc = $formatter->getDoc();
1370  $xpath = new DOMXPath( $doc );
1371  $nodes = $xpath->query( '//a[@href]' );
1373  foreach ( $nodes as $node ) {
1374  $node->setAttribute(
1375  'href',
1376  wfExpandUrl( $node->getAttribute( 'href' ), PROTO_RELATIVE )
1377  );
1378  }
1379  return $formatter->getText( 'html' );
1380  }
1381 
1402  public static function formatComment(
1403  $comment, $title = null, $local = false, $wikiId = null
1404  ) {
1405  $formatter = MediaWikiServices::getInstance()->getCommentFormatter();
1406  return $formatter->format( $comment, $title, $local, $wikiId );
1407  }
1408 
1428  public static function formatLinksInComment(
1429  $comment, $title = null, $local = false, $wikiId = null
1430  ) {
1431  $formatter = MediaWikiServices::getInstance()->getCommentFormatter();
1432  return $formatter->formatLinksUnsafe( $comment, $title, $local, $wikiId );
1433  }
1434 
1441  public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1442  # Valid link forms:
1443  # Foobar -- normal
1444  # :Foobar -- override special treatment of prefix (images, language links)
1445  # /Foobar -- convert to CurrentPage/Foobar
1446  # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1447  # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1448  # ../Foobar -- convert to CurrentPage/Foobar,
1449  # (from CurrentPage/CurrentSubPage)
1450  # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1451  # (from CurrentPage/CurrentSubPage)
1452 
1453  $ret = $target; # default return value is no change
1454 
1455  # Some namespaces don't allow subpages,
1456  # so only perform processing if subpages are allowed
1457  if (
1458  $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1459  hasSubpages( $contextTitle->getNamespace() )
1460  ) {
1461  $hash = strpos( $target, '#' );
1462  if ( $hash !== false ) {
1463  $suffix = substr( $target, $hash );
1464  $target = substr( $target, 0, $hash );
1465  } else {
1466  $suffix = '';
1467  }
1468  # T9425
1469  $target = trim( $target );
1470  $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1471  getPrefixedText( $contextTitle );
1472  # Look at the first character
1473  if ( $target != '' && $target[0] === '/' ) {
1474  # / at end means we don't want the slash to be shown
1475  $m = [];
1476  $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1477  if ( $trailingSlashes ) {
1478  $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1479  } else {
1480  $noslash = substr( $target, 1 );
1481  }
1482 
1483  $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1484  if ( $text === '' ) {
1485  $text = $target . $suffix;
1486  } # this might be changed for ugliness reasons
1487  } else {
1488  # check for .. subpage backlinks
1489  $dotdotcount = 0;
1490  $nodotdot = $target;
1491  while ( str_starts_with( $nodotdot, "../" ) ) {
1492  ++$dotdotcount;
1493  $nodotdot = substr( $nodotdot, 3 );
1494  }
1495  if ( $dotdotcount > 0 ) {
1496  $exploded = explode( '/', $contextPrefixedText );
1497  if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1498  $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1499  # / at the end means don't show full path
1500  if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1501  $nodotdot = rtrim( $nodotdot, '/' );
1502  if ( $text === '' ) {
1503  $text = $nodotdot . $suffix;
1504  }
1505  }
1506  $nodotdot = trim( $nodotdot );
1507  if ( $nodotdot != '' ) {
1508  $ret .= '/' . $nodotdot;
1509  }
1510  $ret .= $suffix;
1511  }
1512  }
1513  }
1514  }
1515 
1516  return $ret;
1517  }
1518 
1538  public static function commentBlock(
1539  $comment, $title = null, $local = false, $wikiId = null, $useParentheses = true
1540  ) {
1541  return MediaWikiServices::getInstance()->getCommentFormatter()
1542  ->formatBlock( $comment, $title, $local, $wikiId, $useParentheses );
1543  }
1544 
1560  public static function revComment(
1561  RevisionRecord $revRecord,
1562  $local = false,
1563  $isPublic = false,
1564  $useParentheses = true
1565  ) {
1566  $authority = RequestContext::getMain()->getAuthority();
1567  $formatter = MediaWikiServices::getInstance()->getCommentFormatter();
1568  return $formatter->formatRevision( $revRecord, $authority, $local, $isPublic, $useParentheses );
1569  }
1570 
1576  public static function formatRevisionSize( $size ) {
1577  if ( $size == 0 ) {
1578  $stxt = wfMessage( 'historyempty' )->escaped();
1579  } else {
1580  $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1581  }
1582  return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1583  }
1584 
1591  public static function tocIndent() {
1592  return "\n<ul>\n";
1593  }
1594 
1602  public static function tocUnindent( $level ) {
1603  return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
1604  }
1605 
1617  public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
1618  $classes = "toclevel-$level";
1619  if ( $sectionIndex !== false ) {
1620  $classes .= " tocsection-$sectionIndex";
1621  }
1622 
1623  // <li class="$classes"><a href="#$anchor"><span class="tocnumber">
1624  // $tocnumber</span> <span class="toctext">$tocline</span></a>
1625  return Html::openElement( 'li', [ 'class' => $classes ] )
1626  . Html::rawElement( 'a',
1627  [ 'href' => "#$anchor" ],
1628  Html::element( 'span', [ 'class' => 'tocnumber' ], $tocnumber )
1629  . ' '
1630  . Html::rawElement( 'span', [ 'class' => 'toctext' ], $tocline )
1631  );
1632  }
1633 
1641  public static function tocLineEnd() {
1642  return "</li>\n";
1643  }
1644 
1653  public static function tocList( $toc, Language $lang = null ) {
1654  $lang = $lang ?? RequestContext::getMain()->getLanguage();
1655 
1656  $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
1657 
1658  return '<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading">'
1659  . Html::element( 'input', [
1660  'type' => 'checkbox',
1661  'role' => 'button',
1662  'id' => 'toctogglecheckbox',
1663  'class' => 'toctogglecheckbox',
1664  'style' => 'display:none',
1665  ] )
1666  . Html::openElement( 'div', [
1667  'class' => 'toctitle',
1668  'lang' => $lang->getHtmlCode(),
1669  'dir' => $lang->getDir(),
1670  ] )
1671  . '<h2 id="mw-toc-heading">' . $title . '</h2>'
1672  . '<span class="toctogglespan">'
1673  . Html::label( '', 'toctogglecheckbox', [
1674  'class' => 'toctogglelabel',
1675  ] )
1676  . '</span>'
1677  . "</div>"
1678  . $toc
1679  . "</ul>\n</div>\n";
1680  }
1681 
1690  public static function generateTOC( $tree, Language $lang = null ) {
1691  $toc = '';
1692  $lastLevel = 0;
1693  foreach ( $tree as $section ) {
1694  if ( $section['toclevel'] > $lastLevel ) {
1695  $toc .= self::tocIndent();
1696  } elseif ( $section['toclevel'] < $lastLevel ) {
1697  $toc .= self::tocUnindent(
1698  $lastLevel - $section['toclevel'] );
1699  } else {
1700  $toc .= self::tocLineEnd();
1701  }
1702 
1703  $toc .= self::tocLine( $section['anchor'],
1704  $section['line'], $section['number'],
1705  $section['toclevel'], $section['index'] );
1706  $lastLevel = $section['toclevel'];
1707  }
1708  $toc .= self::tocLineEnd();
1709  return self::tocList( $toc, $lang );
1710  }
1711 
1728  public static function makeHeadline( $level, $attribs, $anchor, $html,
1729  $link, $fallbackAnchor = false
1730  ) {
1731  $anchorEscaped = htmlspecialchars( $anchor, ENT_COMPAT );
1732  $fallback = '';
1733  if ( $fallbackAnchor !== false && $fallbackAnchor !== $anchor ) {
1734  $fallbackAnchor = htmlspecialchars( $fallbackAnchor, ENT_COMPAT );
1735  $fallback = "<span id=\"$fallbackAnchor\"></span>";
1736  }
1737  return "<h$level$attribs"
1738  . "$fallback<span class=\"mw-headline\" id=\"$anchorEscaped\">$html</span>"
1739  . $link
1740  . "</h$level>";
1741  }
1742 
1749  public static function splitTrail( $trail ) {
1750  $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1751  $inside = '';
1752  if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1753  [ , $inside, $trail ] = $m;
1754  }
1755  return [ $inside, $trail ];
1756  }
1757 
1787  public static function generateRollback(
1788  RevisionRecord $revRecord,
1789  IContextSource $context = null,
1790  $options = [ 'verify' ]
1791  ) {
1792  if ( $context === null ) {
1793  $context = RequestContext::getMain();
1794  }
1795 
1796  $editCount = false;
1797  if ( in_array( 'verify', $options, true ) ) {
1798  $editCount = self::getRollbackEditCount( $revRecord, true );
1799  if ( $editCount === false ) {
1800  return '';
1801  }
1802  }
1803 
1804  $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1805 
1806  // Allow extensions to modify the rollback link.
1807  // Abort further execution if the extension wants full control over the link.
1808  if ( !Hooks::runner()->onLinkerGenerateRollbackLink(
1809  $revRecord, $context, $options, $inner ) ) {
1810  return $inner;
1811  }
1812 
1813  if ( !in_array( 'noBrackets', $options, true ) ) {
1814  $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1815  }
1816 
1817  if ( MediaWikiServices::getInstance()->getUserOptionsLookup()
1818  ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1819  ) {
1820  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1821  $stats->increment( 'rollbackconfirmation.event.load' );
1822  $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1823  }
1824 
1825  return '<span class="mw-rollback-link">' . $inner . '</span>';
1826  }
1827 
1846  public static function getRollbackEditCount( RevisionRecord $revRecord, $verify ) {
1847  $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1848  ->get( MainConfigNames::ShowRollbackEditCount );
1849 
1850  if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1851  // Nothing has happened, indicate this by returning 'null'
1852  return null;
1853  }
1854 
1855  $dbr = wfGetDB( DB_REPLICA );
1856 
1857  // Up to the value of $wgShowRollbackEditCount revisions are counted
1858  $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
1859  // T270033 Index renaming
1860  $revIndex = $dbr->indexExists( 'revision', 'page_timestamp', __METHOD__ )
1861  ? 'page_timestamp'
1862  : 'rev_page_timestamp';
1863  $res = $dbr->select(
1864  $revQuery['tables'],
1865  [ 'rev_user_text' => $revQuery['fields']['rev_user_text'], 'rev_deleted' ],
1866  [ 'rev_page' => $revRecord->getPageId() ],
1867  __METHOD__,
1868  [
1869  'USE INDEX' => [ 'revision' => $revIndex ],
1870  'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ],
1871  'LIMIT' => $showRollbackEditCount + 1
1872  ],
1873  $revQuery['joins']
1874  );
1875 
1876  $revUser = $revRecord->getUser( RevisionRecord::RAW );
1877  $revUserText = $revUser ? $revUser->getName() : '';
1878 
1879  $editCount = 0;
1880  $moreRevs = false;
1881  foreach ( $res as $row ) {
1882  if ( $row->rev_user_text != $revUserText ) {
1883  if ( $verify &&
1884  ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1885  || $row->rev_deleted & RevisionRecord::DELETED_USER
1886  ) ) {
1887  // If the user or the text of the revision we might rollback
1888  // to is deleted in some way we can't rollback. Similar to
1889  // the checks in WikiPage::commitRollback.
1890  return false;
1891  }
1892  $moreRevs = true;
1893  break;
1894  }
1895  $editCount++;
1896  }
1897 
1898  if ( $verify && $editCount <= $showRollbackEditCount && !$moreRevs ) {
1899  // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1900  // and there weren't any other revisions. That means that the current user is the only
1901  // editor, so we can't rollback
1902  return false;
1903  }
1904  return $editCount;
1905  }
1906 
1921  public static function buildRollbackLink(
1922  RevisionRecord $revRecord,
1923  IContextSource $context = null,
1924  $editCount = false
1925  ) {
1926  $config = MediaWikiServices::getInstance()->getMainConfig();
1927  $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1928  $miserMode = $config->get( MainConfigNames::MiserMode );
1929  // To config which pages are affected by miser mode
1930  $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1931 
1932  if ( $context === null ) {
1933  $context = RequestContext::getMain();
1934  }
1935 
1936  $title = $revRecord->getPageAsLinkTarget();
1937  $revUser = $revRecord->getUser();
1938  $revUserText = $revUser ? $revUser->getName() : '';
1939 
1940  $query = [
1941  'action' => 'rollback',
1942  'from' => $revUserText,
1943  'token' => $context->getUser()->getEditToken( 'rollback' ),
1944  ];
1945 
1946  $attrs = [
1947  'data-mw' => 'interface',
1948  'title' => $context->msg( 'tooltip-rollback' )->text()
1949  ];
1950 
1951  $options = [ 'known', 'noclasses' ];
1952 
1953  if ( $context->getRequest()->getBool( 'bot' ) ) {
1954  // T17999
1955  $query['hidediff'] = '1';
1956  $query['bot'] = '1';
1957  }
1958 
1959  if ( $miserMode ) {
1960  foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1961  if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1962  $showRollbackEditCount = false;
1963  break;
1964  }
1965  }
1966  }
1967 
1968  // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1969  $msg = [ 'rollbacklink' ];
1970  if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1971  if ( !is_numeric( $editCount ) ) {
1972  $editCount = self::getRollbackEditCount( $revRecord, false );
1973  }
1974 
1975  if ( $editCount > $showRollbackEditCount ) {
1976  $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1977  } elseif ( $editCount ) {
1978  $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1979  }
1980  }
1981 
1982  $html = $context->msg( ...$msg )->parse();
1983  return self::link( $title, $html, $attrs, $query, $options );
1984  }
1985 
1994  public static function formatHiddenCategories( $hiddencats ) {
1995  $outText = '';
1996  if ( count( $hiddencats ) > 0 ) {
1997  # Construct the HTML
1998  $outText = '<div class="mw-hiddenCategoriesExplanation">';
1999  $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
2000  $outText .= "</div><ul>\n";
2001 
2002  foreach ( $hiddencats as $titleObj ) {
2003  # If it's hidden, it must exist - no need to check with a LinkBatch
2004  $outText .= '<li>'
2005  . self::link( $titleObj, null, [], [], 'known' )
2006  . "</li>\n";
2007  }
2008  $outText .= '</ul>';
2009  }
2010  return $outText;
2011  }
2012 
2016  private static function getContextFromMain() {
2017  $context = RequestContext::getMain();
2018  $context = new DerivativeContext( $context );
2019  return $context;
2020  }
2021 
2039  public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
2040  if ( !$localizer ) {
2041  $localizer = self::getContextFromMain();
2042  }
2043  $message = $localizer->msg( "tooltip-$name", $msgParams );
2044  if ( $message->isDisabled() ) {
2045  $tooltip = false;
2046  } else {
2047  $tooltip = $message->text();
2048  # Compatibility: formerly some tooltips had [alt-.] hardcoded
2049  $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
2050  }
2051 
2052  $options = (array)$options;
2053 
2054  if ( in_array( 'nonexisting', $options ) ) {
2055  $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
2056  }
2057  if ( in_array( 'withaccess', $options ) ) {
2058  $accesskey = self::accesskey( $name, $localizer );
2059  if ( $accesskey !== false ) {
2060  // Should be build the same as in jquery.accessKeyLabel.js
2061  if ( $tooltip === false || $tooltip === '' ) {
2062  $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
2063  } else {
2064  $tooltip .= $localizer->msg( 'word-separator' )->text();
2065  $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
2066  }
2067  }
2068  }
2069 
2070  return $tooltip;
2071  }
2072 
2073  public static $accesskeycache;
2074 
2087  public static function accesskey( $name, $localizer = null ) {
2088  if ( !isset( self::$accesskeycache[$name] ) ) {
2089  if ( !$localizer ) {
2090  $localizer = self::getContextFromMain();
2091  }
2092  $msg = $localizer->msg( "accesskey-$name" );
2093  self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
2094  }
2095  return self::$accesskeycache[$name];
2096  }
2097 
2112  public static function getRevDeleteLink(
2113  Authority $performer,
2114  RevisionRecord $revRecord,
2116  ) {
2117  $canHide = $performer->isAllowed( 'deleterevision' );
2118  $canHideHistory = $performer->isAllowed( 'deletedhistory' );
2119  if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
2120  return '';
2121  }
2122 
2123  if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2124  return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2125  }
2126  $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2127  getPrefixedDBkey( $title );
2128  if ( $revRecord->getId() ) {
2129  // RevDelete links using revision ID are stable across
2130  // page deletion and undeletion; use when possible.
2131  $query = [
2132  'type' => 'revision',
2133  'target' => $prefixedDbKey,
2134  'ids' => $revRecord->getId()
2135  ];
2136  } else {
2137  // Older deleted entries didn't save a revision ID.
2138  // We have to refer to these by timestamp, ick!
2139  $query = [
2140  'type' => 'archive',
2141  'target' => $prefixedDbKey,
2142  'ids' => $revRecord->getTimestamp()
2143  ];
2144  }
2145  return self::revDeleteLink(
2146  $query,
2147  $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2148  $canHide
2149  );
2150  }
2151 
2164  public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2165  $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2166  $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2167  $html = wfMessage( $msgKey )->escaped();
2168  $tag = $restricted ? 'strong' : 'span';
2169  $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2170  return Xml::tags(
2171  $tag,
2172  [ 'class' => 'mw-revdelundel-link' ],
2173  wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2174  );
2175  }
2176 
2188  public static function revDeleteLinkDisabled( $delete = true ) {
2189  $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2190  $html = wfMessage( $msgKey )->escaped();
2191  $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2192  return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2193  }
2194 
2206  private static function updateWatchstarTooltipMessage(
2207  string &$tooltip, array &$msgParams, $config, $user, $relevantTitle
2208  ): void {
2209  if ( !$config || !$user || !$relevantTitle ) {
2210  $mainContext = self::getContextFromMain();
2211  if ( !$config ) {
2212  $config = $mainContext->getConfig();
2213  }
2214  if ( !$user ) {
2215  $user = $mainContext->getUser();
2216  }
2217  if ( !$relevantTitle ) {
2218  $relevantTitle = $mainContext->getSkin()->getRelevantTitle();
2219  }
2220  }
2221 
2222  $isWatchlistExpiryEnabled = $config->get( MainConfigNames::WatchlistExpiry );
2223  if ( !$isWatchlistExpiryEnabled || !$relevantTitle || !$relevantTitle->canExist() ) {
2224  return;
2225  }
2226 
2227  $watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
2228  $watchedItem = $watchStore->getWatchedItem( $user, $relevantTitle );
2229  if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() !== null ) {
2230  $diffInDays = $watchedItem->getExpiryInDays();
2231 
2232  if ( $diffInDays ) {
2233  $msgParams = [ $diffInDays ];
2234  // Resolves to tooltip-ca-unwatch-expiring message
2235  $tooltip = 'ca-unwatch-expiring';
2236  } else { // Resolves to tooltip-ca-unwatch-expiring-hours message
2237  $tooltip = 'ca-unwatch-expiring-hours';
2238  }
2239  }
2240  }
2241 
2258  public static function tooltipAndAccesskeyAttribs(
2259  $name,
2260  array $msgParams = [],
2261  $options = null,
2262  $localizer = null,
2263  $user = null,
2264  $config = null,
2265  $relevantTitle = null
2266  ) {
2267  $options = (array)$options;
2268  $options[] = 'withaccess';
2269  $tooltipTitle = $name;
2270 
2271  // Get optional parameters from global context if any missing.
2272  if ( !$localizer ) {
2273  $localizer = self::getContextFromMain();
2274  }
2275 
2276  // @since 1.35 - add a WatchlistExpiry feature flag to show new watchstar tooltip message
2277  if ( $name === 'ca-unwatch' ) {
2278  self::updateWatchstarTooltipMessage( $tooltipTitle, $msgParams, $config, $user, $relevantTitle );
2279  }
2280 
2281  $attribs = [
2282  'title' => self::titleAttrib( $tooltipTitle, $options, $msgParams, $localizer ),
2283  'accesskey' => self::accesskey( $name, $localizer )
2284  ];
2285  if ( $attribs['title'] === false ) {
2286  unset( $attribs['title'] );
2287  }
2288  if ( $attribs['accesskey'] === false ) {
2289  unset( $attribs['accesskey'] );
2290  }
2291  return $attribs;
2292  }
2293 
2301  public static function tooltip( $name, $options = null ) {
2302  $tooltip = self::titleAttrib( $name, $options );
2303  if ( $tooltip === false ) {
2304  return '';
2305  }
2306  return Xml::expandAttributes( [
2307  'title' => $tooltip
2308  ] );
2309  }
2310 
2311 }
UserOptionsLookup $userOptionsLookup
const NS_USER
Definition: Defines.php:66
const NS_FILE
Definition: Defines.php:70
const NS_MAIN
Definition: Defines.php:64
const PROTO_RELATIVE
Definition: Defines.php:194
const NS_USER_TALK
Definition: Defines.php:67
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$fallback
Definition: MessagesAb.php:11
$success
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:486
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition: Setup.php:486
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
An IContextSource implementation which will inherit context from another source but allow individual ...
static getUserLinkTitle( $userName)
Get a target Title to link a username.
static isExternal( $username)
Tells whether the username is external or not.
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
static label( $label, $id, array $attribs=[])
Convenience function for generating a label for inputs.
Definition: Html.php:835
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:256
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:45
Some internal bits split of from Skin.php.
Definition: Linker.php:40
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2087
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition: Linker.php:948
static link( $target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition: Linker.php:89
static fnamePart( $url)
Returns the filename part of an url.
Definition: Linker.php:222
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1617
static getContextFromMain()
Definition: Linker.php:2016
static userLink( $userId, $userName, $altUserName=false)
Make user link (or user contributions for unregistered users)
Definition: Linker.php:1069
static $accesskeycache
Definition: Linker.php:2073
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition: Linker.php:1367
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition: Linker.php:1304
static specialLink( $name, $key='')
Make a link to a special page given its name and, optionally, a message key from the link text.
Definition: Linker.php:990
static getRollbackEditCount(RevisionRecord $revRecord, $verify)
This function will return the number of revisions which a rollback would revert and,...
Definition: Linker.php:1846
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:141
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition: Linker.php:242
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition: Linker.php:1560
static revUserLink(RevisionRecord $revRecord, $isPublic=false)
Generate a user link if the current user is allowed to view it.
Definition: Linker.php:1276
static processResponsiveImages( $file, $thumb, $hp)
Process responsive images: add 1.5x and 2x subimages to the thumbnail, where applicable.
Definition: Linker.php:789
static blockLink( $userId, $userText)
Definition: Linker.php:1229
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition: Linker.php:2188
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition: Linker.php:1441
const TOOL_LINKS_NOBLOCK
Flags for userToolLinks()
Definition: Linker.php:44
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[])
Make a "broken" link to an image.
Definition: Linker.php:825
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2112
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:163
static getUploadUrl( $destFile, $query='')
Get the URL to upload a certain file.
Definition: Linker.php:898
static tocIndent()
Add another level to the Table of Contents.
Definition: Linker.php:1591
static makeImageLink(Parser $parser, LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
Definition: Linker.php:299
static updateWatchstarTooltipMessage(string &$tooltip, array &$msgParams, $config, $user, $relevantTitle)
Updates the tooltip message and its parameters if watchlist expiry is enabled.
Definition: Linker.php:2206
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition: Linker.php:187
static emailLink( $userId, $userText)
Definition: Linker.php:1250
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1787
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:1994
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:1749
static generateTOC( $tree, Language $lang=null)
Generate a table of contents from a section tree.
Definition: Linker.php:1690
static titleAttrib( $name, $options=null, array $msgParams=[], $localizer=null)
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2039
const TOOL_LINKS_EMAIL
Definition: Linker.php:45
static makeThumbLinkObj(LinkTarget $title, $file, $label='', $alt='', $align=null, $params=[], $framed=false, $manualthumb="")
Make HTML for a thumbnail including image, border and caption.
Definition: Linker.php:575
static formatRevisionSize( $size)
Definition: Linker.php:1576
static commentBlock( $comment, $title=null, $local=false, $wikiId=null, $useParentheses=true)
Wrap a comment in standard punctuation and formatting if it's non-empty, otherwise return empty strin...
Definition: Linker.php:1538
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition: Linker.php:2301
static userTalkLink( $userId, $userText)
Definition: Linker.php:1207
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", array $classes=[], ?Parser $parser=null)
Definition: Linker.php:608
static buildRollbackLink(RevisionRecord $revRecord, IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition: Linker.php:1921
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1324
static formatLinksInComment( $comment, $title=null, $local=false, $wikiId=null)
Formats wiki links and media links in text; all other wiki formatting is ignored.
Definition: Linker.php:1428
static makeMediaLinkObj( $title, $html='', $time=false)
Create a direct link to a given uploaded file.
Definition: Linker.php:929
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:1016
static userToolLinks( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null, $useParentheses=true)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition: Linker.php:1114
static normaliseSpecialPage(LinkTarget $target)
Definition: Linker.php:208
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1728
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1602
static tocList( $toc, Language $lang=null)
Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript.
Definition: Linker.php:1653
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition: Linker.php:532
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition: Linker.php:2164
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null, $localizer=null, $user=null, $config=null, $relevantTitle=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2258
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1641
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition: Linker.php:1402
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition: Linker.php:1195
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Page revision base class.
getUser( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
getVisibility()
Get the deletion bitfield of the revision.
getPageId( $wikiId=self::LOCAL)
Get the page ID.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
static numParam( $num)
Definition: Message.php:1166
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:95
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:1182
static getExternalLinkRel( $url=false, LinkTarget $title=null)
Get the rel attribute for a particular external link.
Definition: Parser.php:2258
static getMain()
Get the RequestContext object associated with the main request.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
static tryNew( $namespace, $title, $fragment='', $interwiki='')
Constructs a TitleValue, or returns null if the parameters are not valid.
Definition: TitleValue.php:80
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:282
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:306
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:638
Representation of a pair of user and title for watchlist entries.
Definition: WatchedItem.php:36
getExpiry(?int $style=TS_MW)
When the watched item will expire.
static expandAttributes( $attribs)
Given an array of ('attributename' => 'value'), it generates the code to set the XML attributes : att...
Definition: Xml.php:69
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:134
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
msg( $key,... $params)
This is the method for getting translated interface messages.
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
const DB_REPLICA
Definition: defines.php:26
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
if(!isset( $args[0])) $lang
$revQuery