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