MediaWiki master
Linker.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Linker;
10
33use Wikimedia\Assert\Assert;
35use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
37use Wikimedia\RemexHtml\Serializer\SerializerNode;
38
48class Linker {
52 public const TOOL_LINKS_NOBLOCK = 1;
53 public const TOOL_LINKS_EMAIL = 2;
54
96 public static function link(
97 $target, $html = null, $customAttribs = [], $query = [], $options = []
98 ) {
99 if ( !$target instanceof LinkTarget ) {
100 wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
101 return "<!-- ERROR -->$html";
102 }
103
104 $options = (array)$options;
105 $linkRenderer = self::getLinkRenderer( $options );
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
147 public static function linkKnown(
148 $target, $html = null, $customAttribs = [], $query = [], $options = [ 'known' ]
149 ) {
150 wfDeprecated( __METHOD__, '1.28' ); // since 1.47
151 return self::getLinkRenderer( $options )->makeKnownLink( $target, $html, $customAttribs, $query );
152 }
153
171 public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '', $hash = '' ) {
172 $nt = Title::newFromLinkTarget( $nt );
173 $attrs = [];
174 if ( $hash ) {
175 $attrs['class'] = 'mw-selflink-fragment';
176 $attrs['href'] = '#' . $hash;
177 } else {
178 // For backwards compatibility with gadgets we add selflink as well.
179 $attrs['class'] = 'mw-selflink selflink';
180 }
181 $ret = Html::rawElement( 'a', $attrs, $prefix . $html ) . $trail;
182 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
183 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
184 return $ret;
185 }
186
187 if ( $html == '' ) {
188 $html = htmlspecialchars( $nt->getPrefixedText() );
189 }
190 [ $inside, $trail ] = self::splitTrail( $trail );
191 return Html::rawElement( 'a', $attrs, $prefix . $html . $inside ) . $trail;
192 }
193
204 public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
205 // First we check whether the namespace exists or not.
206 if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
207 if ( $namespace == NS_MAIN ) {
208 $name = $context->msg( 'blanknamespace' )->text();
209 } else {
210 $name = MediaWikiServices::getInstance()->getContentLanguage()->
211 getFormattedNsText( $namespace );
212 }
213 return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
214 }
215
216 return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
217 }
218
227 private static function fnamePart( $url ) {
228 $basename = strrchr( $url, '/' );
229 if ( $basename === false ) {
230 $basename = $url;
231 } else {
232 $basename = substr( $basename, 1 );
233 }
234 return $basename;
235 }
236
247 public static function makeExternalImage( $url, $alt = '' ) {
248 if ( $alt == '' ) {
249 $alt = self::fnamePart( $url );
250 }
251 $img = '';
252 $success = ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
253 ->onLinkerMakeExternalImage( $url, $alt, $img );
254 if ( !$success ) {
255 wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
256 . "with url {$url} and alt text {$alt} to {$img}" );
257 return $img;
258 }
259 return Html::element( 'img',
260 [
261 'src' => $url,
262 'alt' => $alt
263 ]
264 );
265 }
266
305 public static function makeImageLink( Parser $parser, LinkTarget $title,
306 $file, $frameParams = [], $handlerParams = [], $time = false,
307 $query = '', $widthOption = null
308 ) {
309 $title = Title::newFromLinkTarget( $title );
310 $res = null;
311 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
312 if ( !$hookRunner->onImageBeforeProduceHTML( null, $title,
313 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
314 $file, $frameParams, $handlerParams, $time, $res,
315 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
316 $parser, $query, $widthOption )
317 ) {
318 return $res;
319 }
320
321 if ( $file && !$file->allowInlineDisplay() ) {
322 wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ' does not allow inline display' );
323 return self::link( $title );
324 }
325
326 // Clean up parameters
327 $page = $handlerParams['page'] ?? false;
328 if ( !isset( $frameParams['align'] ) ) {
329 $frameParams['align'] = '';
330 }
331 if ( !isset( $frameParams['title'] ) ) {
332 $frameParams['title'] = '';
333 }
334 if ( !isset( $frameParams['class'] ) ) {
335 $frameParams['class'] = '';
336 }
337
338 $services = MediaWikiServices::getInstance();
339 $config = $services->getMainConfig();
340
341 $classes = [];
342 if (
343 !isset( $handlerParams['width'] ) &&
344 !isset( $handlerParams['height'] ) &&
345 !isset( $frameParams['manualthumb'] ) &&
346 !isset( $frameParams['framed'] )
347 ) {
348 $classes[] = 'mw-default-size';
349 }
350
351 $prefix = $postfix = '';
352
353 if ( $file && !isset( $handlerParams['width'] ) ) {
354 if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
355 // If its a vector image, and user only specifies height
356 // we don't want it to be limited by its "normal" width.
357 $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
358 $handlerParams['width'] = $svgMaxSize;
359 } else {
360 $handlerParams['width'] = $file->getWidth( $page );
361 }
362
363 if ( isset( $frameParams['thumbnail'] )
364 || isset( $frameParams['manualthumb'] )
365 || isset( $frameParams['framed'] )
366 || isset( $frameParams['frameless'] )
367 || !$handlerParams['width']
368 ) {
369 $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
370 $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
371 if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
372 $userOptionsLookup = $services->getUserOptionsLookup();
373 $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
374 }
375
376 // Reduce width for upright images when parameter 'upright' is used
377 if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
378 $frameParams['upright'] = $thumbUpright;
379 }
380
381 // For caching health: If width scaled down due to upright
382 // parameter, round to full __0 pixel to avoid the creation of a
383 // lot of odd thumbs.
384 $prefWidth = isset( $frameParams['upright'] ) ?
385 round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
386 $thumbLimits[$widthOption];
387
388 // Use width which is smaller: real image width or user preference width
389 // Unless image is scalable vector.
390 if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
391 $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
392 $handlerParams['width'] = $prefWidth;
393 }
394 }
395 }
396
397 // Parser::makeImage has a similarly named variable
398 $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
399 isset( $frameParams['manualthumb'] ) ||
400 isset( $frameParams['framed'] );
401
402 if ( $hasVisibleCaption ) {
403 return $prefix . self::makeThumbLink2(
404 $title, $file, $frameParams, $handlerParams, $time, $query,
405 $classes, $parser
406 ) . $postfix;
407 }
408
409 $rdfaType = 'mw:File';
410
411 if ( isset( $frameParams['frameless'] ) ) {
412 $rdfaType .= '/Frameless';
413 if ( $file ) {
414 $srcWidth = $file->getWidth( $page );
415 # For "frameless" option: do not present an image bigger than the
416 # source (for bitmap-style images). This is the same behavior as the
417 # "thumb" option does it already.
418 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
419 $handlerParams['width'] = $srcWidth;
420 }
421 }
422 }
423
424 if ( $file && isset( $handlerParams['width'] ) ) {
425 # Create a resized image, without the additional thumbnail features
426 $thumb = $file->transform( $handlerParams );
427 } else {
428 $thumb = false;
429 }
430
431 $isBadFile = $file && $thumb &&
432 $parser->getBadFileLookup()->isBadFile( $title->getDBkey(), $parser->getTitle() );
433
434 if ( !$thumb || $thumb->isError() || $isBadFile ) {
435 $rdfaType = 'mw:Error ' . $rdfaType;
436 $currentExists = $file && $file->exists();
437 if ( $currentExists && !$thumb ) {
438 $label = wfMessage( 'thumbnail_error', '' )->text();
439 } elseif ( $thumb && $thumb->isError() ) {
440 Assert::invariant(
441 $thumb instanceof MediaTransformError,
442 'Unknown MediaTransformOutput: ' . get_class( $thumb )
443 );
444 $label = $thumb->toText();
445 } else {
446 $label = $frameParams['alt'] ?? '';
447 }
449 $title, $label, '', '', '', (bool)$time, $handlerParams, $currentExists
450 );
451 } else {
452 self::processResponsiveImages( $file, $thumb, $handlerParams );
453 $params = [];
454 // An empty alt indicates an image is not a key part of the content
455 // and that non-visual browsers may omit it from rendering. Only
456 // set the parameter if it's explicitly requested.
457 if ( isset( $frameParams['alt'] ) ) {
458 $params['alt'] = $frameParams['alt'];
459 }
460 $params['title'] = $frameParams['title'];
461 $params += [
462 'img-class' => 'mw-file-element',
463 ];
464 if (
465 isset( $frameParams['upright'] ) &&
466 in_array( 'mw-default-size', $classes, true ) &&
467 isset( $frameParams['frameless'] )
468 ) {
469 $params['img-class'] .= ' mw-file-upright';
470 $params['style'] = '--mw-file-upright: ' . $frameParams['upright'];
471 }
472 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
473 $s = $thumb->toHtml( $params );
474 }
475
476 $wrapper = 'span';
477 $caption = '';
478
479 if ( $frameParams['align'] != '' ) {
480 $wrapper = 'figure';
481 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
482 $classes[] = "mw-halign-{$frameParams['align']}";
483 $caption = Html::rawElement(
484 'figcaption', [], $frameParams['caption'] ?? ''
485 );
486 } elseif ( isset( $frameParams['valign'] ) ) {
487 // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
488 // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
489 // mw-valign-text-bottom
490 $classes[] = "mw-valign-{$frameParams['valign']}";
491 }
492
493 if ( isset( $frameParams['border'] ) ) {
494 $classes[] = 'mw-image-border';
495 }
496
497 if ( isset( $frameParams['class'] ) ) {
498 $classes[] = $frameParams['class'];
499 }
500
501 $attribs = [
502 'class' => $classes,
503 'typeof' => $rdfaType,
504 ];
505
506 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
507
508 return str_replace( "\n", ' ', $s );
509 }
510
519 public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
520 $mtoParams = [];
521 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
522 $mtoParams['custom-url-link'] = $frameParams['link-url'];
523 if ( isset( $frameParams['link-target'] ) ) {
524 $mtoParams['custom-target-link'] = $frameParams['link-target'];
525 }
526 if ( $parser ) {
527 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
528 foreach ( $extLinkAttrs as $name => $val ) {
529 // Currently could include 'rel' and 'target'
530 $mtoParams['parser-extlink-' . $name] = $val;
531 }
532 }
533 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
534 $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
535 self::getLinkRenderer()->normalizeTarget( $frameParams['link-title'] )
536 );
537 if ( isset( $frameParams['link-title-query'] ) ) {
538 $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
539 }
540 } elseif ( !empty( $frameParams['no-link'] ) ) {
541 // No link
542 } else {
543 $mtoParams['desc-link'] = true;
544 $mtoParams['desc-query'] = $query;
545 }
546 return $mtoParams;
547 }
548
561 public static function makeThumbLinkObj(
562 LinkTarget $title, $file, $label = '', $alt = '', $align = null,
563 $params = [], $framed = false, $manualthumb = ''
564 ) {
565 $frameParams = [
566 'alt' => $alt,
567 'caption' => $label,
568 'align' => $align
569 ];
570 $classes = [];
571 if ( $manualthumb ) {
572 $frameParams['manualthumb'] = $manualthumb;
573 } elseif ( $framed ) {
574 $frameParams['framed'] = true;
575 } elseif ( !isset( $params['width'] ) && !isset( $params['height'] ) ) {
576 $classes[] = 'mw-default-size';
577 }
579 $title, $file, $frameParams, $params, false, '', $classes
580 );
581 }
582
594 public static function makeThumbLink2(
595 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
596 $time = false, $query = '', array $classes = [], ?Parser $parser = null
597 ) {
598 $exists = $file && $file->exists();
599 $services = MediaWikiServices::getInstance();
600
601 $page = $handlerParams['page'] ?? false;
602 $lang = $handlerParams['lang'] ?? false;
603
604 if ( !isset( $frameParams['align'] ) ) {
605 $frameParams['align'] = '';
606 }
607 if ( !isset( $frameParams['caption'] ) ) {
608 $frameParams['caption'] = '';
609 }
610
611 if ( empty( $handlerParams['width'] ) ) {
612 // Reduce width for upright images when parameter 'upright' is used
613 $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
614 }
615
616 $thumb = false;
617 $noscale = false;
618 $manualthumb = false;
619 $manual_title = '';
620 $rdfaType = 'mw:File/Thumb';
621
622 if ( !$exists ) {
623 // Same precedence as the $exists case
624 if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
625 $rdfaType = 'mw:File/Frame';
626 }
627 $outerWidth = $handlerParams['width'] + 2;
628 } else {
629 if ( isset( $frameParams['manualthumb'] ) ) {
630 # Use manually specified thumbnail
631 $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
632 if ( $manual_title ) {
633 $manual_img = $services->getRepoGroup()
634 ->findFile( $manual_title );
635 if ( $manual_img ) {
636 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
637 $manualthumb = true;
638 }
639 }
640 } else {
641 $srcWidth = $file->getWidth( $page );
642 if ( isset( $frameParams['framed'] ) ) {
643 $rdfaType = 'mw:File/Frame';
644 if ( !$file->isVectorized() ) {
645 // Use image dimensions, don't scale
646 $noscale = true;
647 } else {
648 // framed is unscaled, but for vectorized images
649 // we need to a width for scaling up for the high density variants
650 $handlerParams['width'] = $srcWidth;
651 }
652 }
653
654 // Do not present an image bigger than the source, for bitmap-style images
655 // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
656 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
657 $handlerParams['width'] = $srcWidth;
658 }
659
660 $thumb = $noscale
661 ? $file->getUnscaledThumb( $handlerParams )
662 : $file->transform( $handlerParams );
663 }
664
665 if ( $thumb ) {
666 $outerWidth = $thumb->getWidth() + 2;
667 } else {
668 $outerWidth = $handlerParams['width'] + 2;
669 }
670 }
671
672 if ( $parser && $rdfaType === 'mw:File/Thumb' ) {
673 $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
674 }
675
676 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
677 $linkTitleQuery = [];
678 if ( $page || $lang ) {
679 if ( $page ) {
680 $linkTitleQuery['page'] = $page;
681 }
682 if ( $lang ) {
683 $linkTitleQuery['lang'] = $lang;
684 }
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 $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 $isBadFile = $exists && $thumb && $parser &&
711 $parser->getBadFileLookup()->isBadFile(
712 $manualthumb ? $manual_title->getDBkey() : $title->getDBkey(),
713 $parser->getTitle()
714 );
715
716 if ( !$exists ) {
717 $rdfaType = 'mw:Error ' . $rdfaType;
718 $label = $frameParams['alt'] ?? '';
720 $title, $label, '', '', '', (bool)$time, $handlerParams, false
721 );
722 $zoomIcon = '';
723 } elseif ( !$thumb || $thumb->isError() || $isBadFile ) {
724 $rdfaType = 'mw:Error ' . $rdfaType;
725 if ( $thumb && $thumb->isError() ) {
726 Assert::invariant(
727 $thumb instanceof MediaTransformError,
728 'Unknown MediaTransformOutput: ' . get_class( $thumb )
729 );
730 $label = $thumb->toText();
731 } elseif ( !$thumb ) {
732 $label = wfMessage( 'thumbnail_error', '' )->text();
733 } else {
734 $label = '';
735 }
737 $title, $label, '', '', '', (bool)$time, $handlerParams, true
738 );
739 $zoomIcon = '';
740 } else {
741 if ( !$noscale && !$manualthumb ) {
742 self::processResponsiveImages( $file, $thumb, $handlerParams );
743 }
744 $params = [];
745 // An empty alt indicates an image is not a key part of the content
746 // and that non-visual browsers may omit it from rendering. Only
747 // set the parameter if it's explicitly requested.
748 if ( isset( $frameParams['alt'] ) ) {
749 $params['alt'] = $frameParams['alt'];
750 }
751 $params += [
752 'img-class' => 'mw-file-element',
753 ];
754 if (
755 isset( $frameParams['upright'] ) &&
756 in_array( 'mw-default-size', $classes, true )
757 ) {
758 $params['img-class'] .= ' mw-file-upright';
759 $params['style'] = '--mw-file-upright: ' . $frameParams['upright'];
760 }
761 // Only thumbs gets the magnify link
762 if ( $rdfaType === 'mw:File/Thumb' ) {
763 $params['magnify-resource'] = $url;
764 }
765 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
766 $s .= $thumb->toHtml( $params );
767 if ( isset( $frameParams['framed'] ) ) {
768 $zoomIcon = '';
769 } else {
770 $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
771 Html::rawElement( 'a', [
772 'href' => $url,
773 'class' => 'internal',
774 'title' => wfMessage( 'thumbnail-more' )->text(),
775 ] )
776 );
777 }
778 }
779
780 $s .= Html::rawElement(
781 'figcaption', [], $frameParams['caption'] ?? ''
782 );
783
784 $attribs = [
785 'class' => $classes,
786 'typeof' => $rdfaType,
787 ];
788
789 $s = Html::rawElement( 'figure', $attribs, $s );
790
791 return str_replace( "\n", ' ', $s );
792 }
793
802 public static function processResponsiveImages( $file, $thumb, $hp ) {
803 $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
804 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
805 $hp20 = $hp;
806 $hp20['width'] = $hp['width'] * 2;
807 if ( isset( $hp['height'] ) ) {
808 $hp20['height'] = $hp['height'] * 2;
809 }
810 $thumb20 = $file->transform( $hp20 );
811 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
812 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
813 }
814 }
815 }
816
834 public static function makeBrokenImageLinkObj(
835 $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
836 $time = false, array $handlerParams = [], bool $currentExists = false
837 ) {
838 if ( !$title instanceof ParsoidLinkTarget ) {
839 wfDeprecatedMsg( __METHOD__ . ': Requires $title to be a LinkTarget object', '1.47' );
840 return "<!-- ERROR -->" . htmlspecialchars( $label );
841 }
842 if ( $query !== '' ) {
843 wfDeprecated( __METHOD__ . ' with non-empty query parameter', '1.47' );
844 }
845
846 $title = Title::newFromLinkTarget( $title );
847 $services = MediaWikiServices::getInstance();
848 $mainConfig = $services->getMainConfig();
849 $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
850 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
851 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
852 if ( $label == '' ) {
853 $label = $title->getPrefixedText();
854 }
855
856 $html = Html::element( 'span', [
857 'class' => 'mw-file-element mw-broken-media',
858 // These data attributes are used to dynamically size the span, see T273013
859 'data-width' => $handlerParams['width'] ?? null,
860 'data-height' => $handlerParams['height'] ?? null,
861 ], $label );
862
863 $repoGroup = $services->getRepoGroup();
864 $currentExists = $currentExists ||
865 ( $time && $repoGroup->findFile( $title ) !== false );
866
867 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
868 && !$currentExists
869 ) {
870 if (
871 $title->inNamespace( NS_FILE ) &&
872 $repoGroup->getLocalRepo()->checkRedirect( $title )
873 ) {
874 // We already know it's a redirect, so mark it accordingly
875 return self::link(
876 $title,
877 $html,
878 [ 'class' => 'mw-redirect' ],
879 wfCgiToArray( $query ),
880 [ 'known', 'noclasses' ]
881 );
882 }
883 return Html::rawElement( 'a', [
884 'href' => self::getUploadUrl( $title, $query ),
885 'class' => 'new',
886 'title' => $title->getPrefixedText()
887 ], $html );
888 }
889 return self::link(
890 $title,
891 $html,
892 [],
893 wfCgiToArray( $query ),
894 [ 'known', 'noclasses' ]
895 );
896 }
897
909 public static function getUploadUrl( ParsoidLinkTarget $destFile, string $query = '', bool $prefixedURL = false ) {
910 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
911 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
912 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
913 $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
914 if ( $query != '' ) {
915 wfDeprecated( __METHOD__ . ' with $query parameter', '1.47' );
916 $q .= '&' . $query;
917 }
918
919 if ( $uploadMissingFileUrl ) {
920 return wfAppendQuery( $uploadMissingFileUrl, $q );
921 }
922
923 if ( $uploadNavigationUrl ) {
924 return wfAppendQuery( $uploadNavigationUrl, $q );
925 }
926
927 $upload = SpecialPage::getTitleFor( 'Upload' );
928
929 if ( $prefixedURL ) {
930 return './' . $upload->getPrefixedURL( $q );
931 }
932 return $upload->getLocalURL( $q );
933 }
934
944 public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
945 $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
946 $title, [ 'time' => $time ]
947 );
948 return self::makeMediaLinkFile( $title, $img, $html );
949 }
950
963 public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
964 if ( $file && $file->exists() ) {
965 $url = $file->getUrl();
966 $class = 'internal';
967 } else {
968 $url = self::getUploadUrl( $title );
969 $class = 'new';
970 }
971
972 $alt = $title->getText();
973 if ( $html == '' ) {
974 $html = $alt;
975 }
976
977 $ret = '';
978 $attribs = [
979 'href' => $url,
980 'class' => $class,
981 'title' => $alt
982 ];
983
984 if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
985 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
986 ) {
987 wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
988 . "with url {$url} and text {$html} to {$ret}" );
989 return $ret;
990 }
991
992 return Html::rawElement( 'a', $attribs, $html );
993 }
994
1005 public static function specialLink( $name, $key = '' ) {
1006 $queryPos = strpos( $name, '?' );
1007 if ( $queryPos !== false ) {
1008 $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
1009 $name = substr( $name, 0, $queryPos );
1010 } else {
1011 $getParams = [];
1012 }
1013
1014 $slashPos = strpos( $name, '/' );
1015 if ( $slashPos !== false ) {
1016 $subpage = substr( $name, $slashPos + 1 );
1017 $name = substr( $name, 0, $slashPos );
1018 } else {
1019 $subpage = false;
1020 }
1021
1022 if ( $key == '' ) {
1023 $key = strtolower( $name );
1024 }
1025
1026 return self::getLinkRenderer()->makeKnownLink(
1027 SpecialPage::getTitleFor( $name, $subpage ),
1028 wfMessage( $key )->plain(),
1029 [],
1030 $getParams
1031 );
1032 }
1033
1054 public static function makeExternalLink( $url, $text, $escape = true,
1055 $linktype = '', $attribs = [], $title = null
1056 ) {
1057 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1058 global $wgTitle;
1059 return self::getLinkRenderer()->makeExternalLink(
1060 $url,
1061 $escape ? $text : new HtmlArmor( $text ),
1062 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1063 $linktype,
1064 $attribs
1065 );
1066 }
1067
1082 public static function userLink(
1083 $userId,
1084 $userName,
1085 $altUserName = false,
1086 $attributes = []
1087 ) {
1088 if ( $userName === '' || $userName === false || $userName === null ) {
1089 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1090 'that need to be fixed?' );
1091 return wfMessage( 'empty-username' )->parse();
1092 }
1093
1094 return self::getLinkRenderer()->makeUserLink(
1095 new UserIdentityValue( $userId, (string)$userName ),
1096 RequestContext::getMain(),
1097 $altUserName === false ? null : (string)$altUserName,
1098 $attributes
1099 );
1100 }
1101
1120 public static function userToolLinkArray(
1121 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1122 ): array {
1123 $services = MediaWikiServices::getInstance();
1124 $disableAnonTalk = $services->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
1139 // (T412013) Do not link to Special:Contributions for temp accounts
1140 // since the target for the link in the username itself already links to
1141 // Special:Contributions.
1142 if ( $userId && !$services->getTempUserConfig()->isTempName( $userText ) ) {
1143 $attribs = [];
1144 $attribs['class'] = 'mw-usertoollinks-contribs';
1145
1146 // check if the user has edits
1147 if ( $redContribsWhenNoEdits ) {
1148 if ( $edits === null ) {
1149 $user = UserIdentityValue::newRegistered( $userId, $userText );
1150 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1151 }
1152 if ( $edits === 0 ) {
1153 // Note: "new" class is inappropriate here, as "new" class
1154 // should only be used for pages that do not exist.
1155 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1156 }
1157 }
1158 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1159
1160 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1161 }
1162 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1163 if ( $blockable && $userCanBlock ) {
1164 $items[] = self::blockLink( $userId, $userText );
1165 }
1166
1167 if (
1168 $addEmailLink
1169 && MediaWikiServices::getInstance()->getEmailUserFactory()
1170 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1171 ->canSend()
1172 ->isGood()
1173 ) {
1174 $items[] = self::emailLink( $userId, $userText );
1175 }
1176
1177 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1178
1179 return $items;
1180 }
1181
1189 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1190 if ( !$items ) {
1191 return '';
1192 }
1193
1194 if ( $useParentheses ) {
1195 $lang = RequestContext::getMain()->getLanguage();
1196 return wfMessage( 'word-separator' )->escaped()
1197 . '<span class="mw-usertoollinks">'
1198 . wfMessage( 'parentheses' )->rawParams( $lang->pipeList( $items ) )->escaped()
1199 . '</span>';
1200 }
1201
1202 $tools = [];
1203 foreach ( $items as $tool ) {
1204 $tools[] = Html::rawElement( 'span', [], $tool );
1205 }
1206 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1207 implode( ' ', $tools ) . '</span>';
1208 }
1209
1224 public static function userToolLinks(
1225 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1226 $useParentheses = true
1227 ) {
1228 if ( $userText === '' ) {
1229 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1230 'that need to be fixed?' );
1231 return ' ' . wfMessage( 'empty-username' )->parse();
1232 }
1233
1234 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1235 return self::renderUserToolLinksArray( $items, $useParentheses );
1236 }
1237
1247 public static function userToolLinksRedContribs(
1248 $userId, $userText, $edits = null, $useParentheses = true
1249 ) {
1250 return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1251 }
1252
1259 public static function userTalkLink( $userId, $userText ) {
1260 if ( $userText === '' ) {
1261 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1262 'that need to be fixed?' );
1263 return wfMessage( 'empty-username' )->parse();
1264 }
1265
1266 $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1267 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1268 $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1269
1270 return $userTalkPage
1271 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1272 : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1273 }
1274
1281 public static function blockLink( $userId, $userText ) {
1282 if ( $userText === '' ) {
1283 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1284 'that need to be fixed?' );
1285 return wfMessage( 'empty-username' )->parse();
1286 }
1287
1288 $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1289 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1290
1291 return self::link( $blockPage,
1292 wfMessage( 'blocklink' )->escaped(),
1293 $moreLinkAttribs
1294 );
1295 }
1296
1302 public static function emailLink( $userId, $userText ) {
1303 if ( $userText === '' ) {
1304 wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1305 'that need to be fixed?' );
1306 return wfMessage( 'empty-username' )->parse();
1307 }
1308
1309 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1310 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1311 return self::link( $emailPage,
1312 wfMessage( 'emaillink' )->escaped(),
1313 $moreLinkAttribs
1314 );
1315 }
1316
1328 public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1329 // TODO inject authority
1330 $authority = RequestContext::getMain()->getAuthority();
1331
1332 $revUser = $revRecord->getUser(
1333 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1334 $authority
1335 );
1336 if ( $revUser ) {
1337 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1338 } else {
1339 // User is deleted and we can't (or don't want to) view it
1340 $link = wfMessage( 'rev-deleted-user' )->escaped();
1341 }
1342
1343 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1344 $class = self::getRevisionDeletedClass( $revRecord );
1345 return '<span class="' . $class . '">' . $link . '</span>';
1346 }
1347 return $link;
1348 }
1349
1356 public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1357 $class = 'history-deleted';
1358 if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1359 $class .= ' mw-history-suppressed';
1360 }
1361 return $class;
1362 }
1363
1376 public static function revUserTools(
1377 RevisionRecord $revRecord,
1378 $isPublic = false,
1379 $useParentheses = true
1380 ) {
1381 // TODO inject authority
1382 $authority = RequestContext::getMain()->getAuthority();
1383
1384 $revUser = $revRecord->getUser(
1385 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1386 $authority
1387 );
1388 if ( $revUser ) {
1389 $link = self::userLink(
1390 $revUser->getId(),
1391 $revUser->getName(),
1392 false,
1393 [ 'data-mw-revid' => $revRecord->getId() ]
1394 ) . self::userToolLinks(
1395 $revUser->getId(),
1396 $revUser->getName(),
1397 false,
1398 0,
1399 null,
1400 $useParentheses
1401 );
1402 } else {
1403 // User is deleted and we can't (or don't want to) view it
1404 $link = wfMessage( 'rev-deleted-user' )->escaped();
1405 }
1406
1407 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1408 $class = self::getRevisionDeletedClass( $revRecord );
1409 return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1410 }
1411 return $link;
1412 }
1413
1424 public static function expandLocalLinks( string $html ) {
1425 return HtmlHelper::modifyElements(
1426 $html,
1427 static function ( SerializerNode $node ): bool {
1428 return $node->name === 'a' && isset( $node->attrs['href'] );
1429 },
1430 static function ( SerializerNode $node ): SerializerNode {
1431 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1432 $href = $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE );
1433 if ( $href !== null ) {
1434 $node->attrs['href'] = $href;
1435 }
1436 return $node;
1437 }
1438 );
1439 }
1440
1447 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1448 # Valid link forms:
1449 # Foobar -- normal
1450 # :Foobar -- override special treatment of prefix (images, language links)
1451 # /Foobar -- convert to CurrentPage/Foobar
1452 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1453 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1454 # ../Foobar -- convert to CurrentPage/Foobar,
1455 # (from CurrentPage/CurrentSubPage)
1456 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1457 # (from CurrentPage/CurrentSubPage)
1458
1459 $ret = $target; # default return value is no change
1460
1461 # Some namespaces don't allow subpages,
1462 # so only perform processing if subpages are allowed
1463 if (
1464 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1465 hasSubpages( $contextTitle->getNamespace() )
1466 ) {
1467 $hash = strpos( $target, '#' );
1468 if ( $hash !== false ) {
1469 $suffix = substr( $target, $hash );
1470 $target = substr( $target, 0, $hash );
1471 } else {
1472 $suffix = '';
1473 }
1474 # T9425
1475 $target = trim( $target );
1476 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1477 getPrefixedText( $contextTitle );
1478 # Look at the first character
1479 if ( $target != '' && $target[0] === '/' ) {
1480 # / at end means we don't want the slash to be shown
1481 $m = [];
1482 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1483 if ( $trailingSlashes ) {
1484 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1485 } else {
1486 $noslash = substr( $target, 1 );
1487 }
1488
1489 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1490 if ( $text === '' ) {
1491 $text = $target . $suffix;
1492 } # this might be changed for ugliness reasons
1493 } else {
1494 # check for .. subpage backlinks
1495 $dotdotcount = 0;
1496 $nodotdot = $target;
1497 while ( str_starts_with( $nodotdot, '../' ) ) {
1498 ++$dotdotcount;
1499 $nodotdot = substr( $nodotdot, 3 );
1500 }
1501 if ( $dotdotcount > 0 ) {
1502 $exploded = explode( '/', $contextPrefixedText );
1503 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1504 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1505 # / at the end means don't show full path
1506 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1507 $nodotdot = rtrim( $nodotdot, '/' );
1508 if ( $text === '' ) {
1509 $text = $nodotdot . $suffix;
1510 }
1511 }
1512 $nodotdot = trim( $nodotdot );
1513 if ( $nodotdot != '' ) {
1514 $ret .= '/' . $nodotdot;
1515 }
1516 $ret .= $suffix;
1517 }
1518 }
1519 }
1520 }
1521
1522 return $ret;
1523 }
1524
1530 public static function formatRevisionSize( $size ) {
1531 if ( $size == 0 ) {
1532 $stxt = wfMessage( 'historyempty' )->escaped();
1533 } else {
1534 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1535 }
1536 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1537 }
1538
1545 public static function splitTrail( $trail ) {
1546 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1547 $inside = '';
1548 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1549 [ , $inside, $trail ] = $m;
1550 }
1551 return [ $inside, $trail ];
1552 }
1553
1584 public static function generateRollback(
1585 RevisionRecord $revRecord,
1586 ?IContextSource $context = null,
1587 $options = []
1588 ) {
1589 $context ??= RequestContext::getMain();
1590
1591 $editCount = self::getRollbackEditCount( $revRecord );
1592 if ( $editCount === false ) {
1593 return '';
1594 }
1595
1596 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1597
1598 $services = MediaWikiServices::getInstance();
1599 // Allow extensions to modify the rollback link.
1600 // Abort further execution if the extension wants full control over the link.
1601 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1602 $revRecord, $context, $options, $inner ) ) {
1603 return $inner;
1604 }
1605
1606 if ( !in_array( 'noBrackets', $options, true ) ) {
1607 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1608 }
1609
1610 if ( $services->getUserOptionsLookup()
1611 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1612 ) {
1613 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1614 }
1615
1616 return '<span class="mw-rollback-link">' . $inner . '</span>';
1617 }
1618
1637 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1638 if ( func_num_args() > 1 ) {
1639 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1640 }
1641 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1642 ->get( MainConfigNames::ShowRollbackEditCount );
1643
1644 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1645 // Nothing has happened, indicate this by returning 'null'
1646 return null;
1647 }
1648
1649 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1650
1651 // Up to the value of $wgShowRollbackEditCount revisions are counted
1652 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1653 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1654 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1655 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1656 ->limit( $showRollbackEditCount + 1 )
1657 ->caller( __METHOD__ )->fetchResultSet();
1658
1659 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1660 $revUserText = $revUser ? $revUser->getName() : '';
1661
1662 $editCount = 0;
1663 $moreRevs = false;
1664 foreach ( $res as $row ) {
1665 if ( $row->rev_user_text != $revUserText ) {
1666 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1667 || $row->rev_deleted & RevisionRecord::DELETED_USER
1668 ) {
1669 // If the user or the text of the revision we might rollback
1670 // to is deleted in some way we can't rollback. Similar to
1671 // the checks in WikiPage::commitRollback.
1672 return false;
1673 }
1674 $moreRevs = true;
1675 break;
1676 }
1677 $editCount++;
1678 }
1679
1680 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1681 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1682 // and there weren't any other revisions. That means that the current user is the only
1683 // editor, so we can't rollback
1684 return false;
1685 }
1686 return $editCount;
1687 }
1688
1703 public static function buildRollbackLink(
1704 RevisionRecord $revRecord,
1705 ?IContextSource $context = null,
1706 $editCount = false
1707 ) {
1708 $config = MediaWikiServices::getInstance()->getMainConfig();
1709 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1710 $miserMode = $config->get( MainConfigNames::MiserMode );
1711 // To config which pages are affected by miser mode
1712 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1713
1714 $context ??= RequestContext::getMain();
1715
1716 $title = $revRecord->getPageAsLinkTarget();
1717 $revUser = $revRecord->getUser();
1718 $revUserText = $revUser ? $revUser->getName() : '';
1719
1720 $query = [
1721 'action' => 'rollback',
1722 'from' => $revUserText,
1723 'token' => $context->getUser()->getEditToken( 'rollback' ),
1724 ];
1725
1726 $attrs = [
1727 'data-mw-interface' => '',
1728 'title' => $context->msg( 'tooltip-rollback' )->text()
1729 ];
1730
1731 $options = [ 'known', 'noclasses' ];
1732
1733 if ( $context->getRequest()->getBool( 'bot' ) ) {
1734 // T17999
1735 $query['hidediff'] = '1';
1736 $query['bot'] = '1';
1737 }
1738
1739 if ( $miserMode ) {
1740 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1741 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1742 $showRollbackEditCount = false;
1743 break;
1744 }
1745 }
1746 }
1747
1748 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1749 $msg = [ 'rollbacklink' ];
1750 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1751 if ( !is_numeric( $editCount ) ) {
1752 $editCount = self::getRollbackEditCount( $revRecord );
1753 }
1754
1755 if ( $editCount > $showRollbackEditCount ) {
1756 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1757 } elseif ( $editCount ) {
1758 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1759 }
1760 }
1761
1762 $html = $context->msg( ...$msg )->parse();
1763 return self::link( $title, $html, $attrs, $query, $options );
1764 }
1765
1774 public static function formatHiddenCategories( $hiddencats ) {
1775 $outText = '';
1776 if ( count( $hiddencats ) > 0 ) {
1777 # Construct the HTML
1778 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1779 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1780 $outText .= "</div><ul>\n";
1781
1782 foreach ( $hiddencats as $titleObj ) {
1783 # If it's hidden, it must exist - no need to check with a LinkBatch
1784 $outText .= '<li>'
1785 . self::link( $titleObj, null, [], [], 'known' )
1786 . "</li>\n";
1787 }
1788 $outText .= '</ul>';
1789 }
1790 return $outText;
1791 }
1792
1796 private static function getContextFromMain() {
1797 $context = RequestContext::getMain();
1798 // TODO: Why is this here?
1799 $context = new DerivativeContext( $context );
1800 return $context;
1801 }
1802
1820 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1821 if ( !$localizer ) {
1822 $localizer = self::getContextFromMain();
1823 }
1824 $message = $localizer->msg( "tooltip-$name", $msgParams );
1825 // Set a default tooltip for subject namespace tabs if that hasn't
1826 // been defined. See T22126
1827 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1828 $message = $localizer->msg( 'tooltip-ca-nstab' );
1829 }
1830
1831 if ( $message->isDisabled() ) {
1832 $tooltip = false;
1833 } else {
1834 $tooltip = $message->text();
1835 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1836 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1837 }
1838
1839 $options = (array)$options;
1840
1841 if ( in_array( 'nonexisting', $options ) ) {
1842 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1843 }
1844 if ( in_array( 'withaccess', $options ) ) {
1845 $accesskey = self::accesskey( $name, $localizer );
1846 if ( $accesskey !== false ) {
1847 // Should be build the same as in jquery.accessKeyLabel.js
1848 if ( $tooltip === false || $tooltip === '' ) {
1849 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1850 } else {
1851 $tooltip .= $localizer->msg( 'word-separator' )->text();
1852 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1853 }
1854 }
1855 }
1856
1857 return $tooltip;
1858 }
1859
1864 public static $accesskeycache;
1865
1878 public static function accesskey( $name, $localizer = null ) {
1879 // Optimization: Reduce from 79 to 35 fetches
1880 // Vector fetches most accesskeys 2x and "search" 6x (April 2026).
1881 // This cache was added in 2010 (r78995), and worthwhile even with message preload (r52503).
1882 //
1883 // NOTE: This assumes calls won't differ by context (i.e. wiki database, and user language).
1884 if ( !isset( self::$accesskeycache[$name] ) ) {
1885 $localizer ??= RequestContext::getMain();
1886 $msg = $localizer->msg( "accesskey-$name" );
1887 // T22126: For custom namespaces, ensure a default
1888 // Talk pages have their default in SkinTemplate::buildContentNavigationUrlsInternal.
1889 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1890 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1891 }
1892 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1893 }
1894 return self::$accesskeycache[$name];
1895 }
1896
1911 public static function getRevDeleteLink(
1912 Authority $performer,
1913 RevisionRecord $revRecord,
1914 LinkTarget $title
1915 ) {
1916 $canHide = $performer->isAllowed( 'deleterevision' );
1917 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
1918 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
1919 return '';
1920 }
1921
1922 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
1923 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
1924 }
1925 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
1926 getPrefixedDBkey( $title );
1927 if ( $revRecord->getId() ) {
1928 // RevDelete links using revision ID are stable across
1929 // page deletion and undeletion; use when possible.
1930 $query = [
1931 'type' => 'revision',
1932 'target' => $prefixedDbKey,
1933 'ids' => $revRecord->getId()
1934 ];
1935 } else {
1936 // Older deleted entries didn't save a revision ID.
1937 // We have to refer to these by timestamp, ick!
1938 $query = [
1939 'type' => 'archive',
1940 'target' => $prefixedDbKey,
1941 'ids' => $revRecord->getTimestamp()
1942 ];
1943 }
1944 return self::revDeleteLink(
1945 $query,
1946 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
1947 $canHide
1948 );
1949 }
1950
1963 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
1964 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
1965 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1966 $html = wfMessage( $msgKey )->escaped();
1967 $tag = $restricted ? 'strong' : 'span';
1968 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
1969 return Html::rawElement(
1970 $tag,
1971 [ 'class' => 'mw-revdelundel-link' ],
1972 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
1973 );
1974 }
1975
1987 public static function revDeleteLinkDisabled( $delete = true ) {
1988 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1989 $html = wfMessage( $msgKey )->escaped();
1990 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
1991 return Html::rawElement( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
1992 }
1993
2007 public static function tooltipAndAccesskeyAttribs(
2008 $name,
2009 array $msgParams = [],
2010 $options = null,
2011 $localizer = null
2012 ) {
2013 $options = (array)$options;
2014 $options[] = 'withaccess';
2015
2016 // Get optional parameters from global context if any missing.
2017 if ( !$localizer ) {
2018 $localizer = self::getContextFromMain();
2019 }
2020
2021 $attribs = [
2022 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2023 'accesskey' => self::accesskey( $name, $localizer )
2024 ];
2025 if ( $attribs['title'] === false ) {
2026 unset( $attribs['title'] );
2027 }
2028 if ( $attribs['accesskey'] === false ) {
2029 unset( $attribs['accesskey'] );
2030 }
2031 return $attribs;
2032 }
2033
2041 public static function tooltip( $name, $options = null ) {
2042 $tooltip = self::titleAttrib( $name, $options );
2043 if ( $tooltip === false ) {
2044 return '';
2045 }
2046 return Html::expandAttributes( [
2047 'title' => $tooltip
2048 ] );
2049 }
2050
2055 private static function getLinkRenderer(
2056 array $legacyOptions = []
2057 ): LinkRenderer {
2058 $services = MediaWikiServices::getInstance();
2059
2060 if ( count( $legacyOptions ) > 0 ) {
2061 return $services->getLinkRendererFactory()->createFromLegacyOptions(
2062 $legacyOptions
2063 );
2064 }
2065
2066 return $services->getLinkRenderer();
2067 }
2068
2069}
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const PROTO_RELATIVE
Definition Defines.php:219
const NS_USER_TALK
Definition Defines.php:54
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.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
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.
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli') global $wgTitle
Definition Setup.php:527
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
An IContextSource implementation which will inherit context from another source but allow individual ...
Group all the pieces relevant to the context of a request into one instance.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:80
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Static utilities for manipulating HTML strings.
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:48
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition Linker.php:1424
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:1963
static link( $target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition Linker.php:96
static string false[] $accesskeycache
Definition Linker.php:1864
static blockLink( $userId, $userText)
Definition Linker.php:1281
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='', $hash='')
Make appropriate markup for a link to the current article.
Definition Linker.php:171
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null, $localizer=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2007
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:305
static makeMediaLinkObj( $title, $html='', $time=false)
Create a direct link to a given uploaded file.
Definition Linker.php:944
static processResponsiveImages( $file, $thumb, $hp)
Add 2x variant for srcset, if $wgResponsiveImages is enabled.
Definition Linker.php:802
static userTalkLink( $userId, $userText)
Definition Linker.php:1259
static generateRollback(RevisionRecord $revRecord, ?IContextSource $context=null, $options=[])
Generate a rollback link for a given revision.
Definition Linker.php:1584
static buildRollbackLink(RevisionRecord $revRecord, ?IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1703
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1447
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:1005
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:1224
static emailLink( $userId, $userText)
Definition Linker.php:1302
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1774
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition Linker.php:204
static getRollbackEditCount(RevisionRecord $revRecord, $verify=true)
This function will return the number of revisions which a rollback would revert and will verify that ...
Definition Linker.php:1637
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1054
static getUploadUrl(ParsoidLinkTarget $destFile, string $query='', bool $prefixedURL=false)
Get the URL to upload a certain file.
Definition Linker.php:909
static userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1120
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition Linker.php:519
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition Linker.php:147
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:1911
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query='', array $classes=[], ?Parser $parser=null)
Definition Linker.php:594
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:247
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition Linker.php:2041
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[], bool $currentExists=false)
Make a "broken" link to an image.
Definition Linker.php:834
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:963
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1878
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:1820
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1189
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition Linker.php:1247
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:1545
const TOOL_LINKS_NOBLOCK
Flags for userToolLinks()
Definition Linker.php:52
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:1987
static formatRevisionSize( $size)
Definition Linker.php:1530
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:561
static userLink( $userId, $userName, $altUserName=false, $attributes=[])
Make user link (or user contributions for unregistered users)
Definition Linker.php:1082
static revUserLink(RevisionRecord $revRecord, $isPublic=false)
Generate a user link if the current user is allowed to view it.
Definition Linker.php:1328
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1356
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:1376
A class containing constants representing the names of configuration variables.
const UploadNavigationUrl
Name constant for the UploadNavigationUrl setting, for use with Config::get()
const ThumbUpright
Name constant for the ThumbUpright setting, for use with Config::get()
const EnableUploads
Name constant for the EnableUploads setting, for use with Config::get()
const SVGMaxSize
Name constant for the SVGMaxSize setting, for use with Config::get()
const ResponsiveImages
Name constant for the ResponsiveImages setting, for use with Config::get()
const DisableAnonTalk
Name constant for the DisableAnonTalk setting, for use with Config::get()
const ThumbLimits
Name constant for the ThumbLimits setting, for use with Config::get()
const UploadMissingFileUrl
Name constant for the UploadMissingFileUrl setting, for use with Config::get()
Service locator for MediaWiki core services.
getMainConfig()
Returns the Config object that provides configuration for MediaWiki core.
static getInstance()
Returns the global default instance of the top level service locator.
Basic media transform error class.
Base class for the output of MediaHandler::doTransform() and File::transform().
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:138
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1158
Page revision base class.
userCan(int $field, Authority $performer)
Determine if the given authority is allowed to view a particular field of this revision,...
getUser(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
isDeleted(int $field)
MCR migration note: this replaced Revision::isDeleted.
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.
getId( $wikiId=self::LOCAL)
Get revision ID.
Parent class for all special pages.
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,...
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Class to parse and build external user names.
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
Value object representing a user's identity.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Build SELECT queries with a fluent interface.
Interface for objects which can provide a MediaWiki context on request.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
Represents the target of a wiki link.
getDBkey()
Get the main part of the link target, in canonical database form.
getText()
Get the main part of the link target, in text form.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
element(SerializerNode $parent, SerializerNode $node, $contents)