MediaWiki master
Linker.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Linker;
24
25use File;
26use HtmlArmor;
49use Wikimedia\Assert\Assert;
50use Wikimedia\IPUtils;
52use Wikimedia\RemexHtml\Serializer\SerializerNode;
53
63class Linker {
67 public const TOOL_LINKS_NOBLOCK = 1;
68 public const TOOL_LINKS_EMAIL = 2;
69
111 public static function link(
112 $target, $html = null, $customAttribs = [], $query = [], $options = []
113 ) {
114 if ( !$target instanceof LinkTarget ) {
115 wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
116 return "<!-- ERROR -->$html";
117 }
118
119 $services = MediaWikiServices::getInstance();
120 $options = (array)$options;
121 if ( $options ) {
122 // Custom options, create new LinkRenderer
123 $linkRenderer = $services->getLinkRendererFactory()
124 ->createFromLegacyOptions( $options );
125 } else {
126 $linkRenderer = $services->getLinkRenderer();
127 }
128
129 if ( $html !== null ) {
130 $text = new HtmlArmor( $html );
131 } else {
132 $text = null;
133 }
134
135 if ( in_array( 'known', $options, true ) ) {
136 return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
137 }
138
139 if ( in_array( 'broken', $options, true ) ) {
140 return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
141 }
142
143 if ( in_array( 'noclasses', $options, true ) ) {
144 return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query );
145 }
146
147 return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
148 }
149
169 public static function linkKnown(
170 $target, $html = null, $customAttribs = [],
171 $query = [], $options = [ 'known' ]
172 ) {
173 return self::link( $target, $html, $customAttribs, $query, $options );
174 }
175
193 public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '', $hash = '' ) {
194 $nt = Title::newFromLinkTarget( $nt );
195 $attrs = [];
196 if ( $hash ) {
197 $attrs['class'] = 'mw-selflink-fragment';
198 $attrs['href'] = '#' . $hash;
199 } else {
200 // For backwards compatibility with gadgets we add selflink as well.
201 $attrs['class'] = 'mw-selflink selflink';
202 }
203 $ret = Html::rawElement( 'a', $attrs, $prefix . $html ) . $trail;
204 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
205 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
206 return $ret;
207 }
208
209 if ( $html == '' ) {
210 $html = htmlspecialchars( $nt->getPrefixedText() );
211 }
212 [ $inside, $trail ] = self::splitTrail( $trail );
213 return Html::rawElement( 'a', $attrs, $prefix . $html . $inside ) . $trail;
214 }
215
226 public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
227 // First we check whether the namespace exists or not.
228 if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
229 if ( $namespace == NS_MAIN ) {
230 $name = $context->msg( 'blanknamespace' )->text();
231 } else {
232 $name = MediaWikiServices::getInstance()->getContentLanguage()->
233 getFormattedNsText( $namespace );
234 }
235 return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
236 }
237
238 return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
239 }
240
249 private static function fnamePart( $url ) {
250 $basename = strrchr( $url, '/' );
251 if ( $basename === false ) {
252 $basename = $url;
253 } else {
254 $basename = substr( $basename, 1 );
255 }
256 return $basename;
257 }
258
269 public static function makeExternalImage( $url, $alt = '' ) {
270 if ( $alt == '' ) {
271 $alt = self::fnamePart( $url );
272 }
273 $img = '';
274 $success = ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
275 ->onLinkerMakeExternalImage( $url, $alt, $img );
276 if ( !$success ) {
277 wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
278 . "with url {$url} and alt text {$alt} to {$img}" );
279 return $img;
280 }
281 return Html::element( 'img',
282 [
283 'src' => $url,
284 'alt' => $alt
285 ]
286 );
287 }
288
327 public static function makeImageLink( Parser $parser, LinkTarget $title,
328 $file, $frameParams = [], $handlerParams = [], $time = false,
329 $query = '', $widthOption = null
330 ) {
331 $title = Title::newFromLinkTarget( $title );
332 $res = null;
333 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
334 if ( !$hookRunner->onImageBeforeProduceHTML( null, $title,
335 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
336 $file, $frameParams, $handlerParams, $time, $res,
337 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
338 $parser, $query, $widthOption )
339 ) {
340 return $res;
341 }
342
343 if ( $file && !$file->allowInlineDisplay() ) {
344 wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ' does not allow inline display' );
345 return self::link( $title );
346 }
347
348 // Clean up parameters
349 $page = $handlerParams['page'] ?? false;
350 if ( !isset( $frameParams['align'] ) ) {
351 $frameParams['align'] = '';
352 }
353 if ( !isset( $frameParams['title'] ) ) {
354 $frameParams['title'] = '';
355 }
356 if ( !isset( $frameParams['class'] ) ) {
357 $frameParams['class'] = '';
358 }
359
360 $services = MediaWikiServices::getInstance();
361 $config = $services->getMainConfig();
362 $enableLegacyMediaDOM = $config->get( MainConfigNames::ParserEnableLegacyMediaDOM );
363
364 $classes = [];
365 if (
366 !isset( $handlerParams['width'] ) &&
367 !isset( $frameParams['manualthumb'] ) &&
368 !isset( $frameParams['framed'] )
369 ) {
370 $classes[] = 'mw-default-size';
371 }
372
373 $prefix = $postfix = '';
374
375 if ( $enableLegacyMediaDOM ) {
376 if ( $frameParams['align'] == 'center' ) {
377 $prefix = '<div class="center">';
378 $postfix = '</div>';
379 $frameParams['align'] = 'none';
380 }
381 }
382
383 if ( $file && !isset( $handlerParams['width'] ) ) {
384 if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
385 // If its a vector image, and user only specifies height
386 // we don't want it to be limited by its "normal" width.
387 $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
388 $handlerParams['width'] = $svgMaxSize;
389 } else {
390 $handlerParams['width'] = $file->getWidth( $page );
391 }
392
393 if ( isset( $frameParams['thumbnail'] )
394 || isset( $frameParams['manualthumb'] )
395 || isset( $frameParams['framed'] )
396 || isset( $frameParams['frameless'] )
397 || !$handlerParams['width']
398 ) {
399 $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
400 $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
401 if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
402 $userOptionsLookup = $services->getUserOptionsLookup();
403 $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
404 }
405
406 // Reduce width for upright images when parameter 'upright' is used
407 if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
408 $frameParams['upright'] = $thumbUpright;
409 }
410
411 // For caching health: If width scaled down due to upright
412 // parameter, round to full __0 pixel to avoid the creation of a
413 // lot of odd thumbs.
414 $prefWidth = isset( $frameParams['upright'] ) ?
415 round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
416 $thumbLimits[$widthOption];
417
418 // Use width which is smaller: real image width or user preference width
419 // Unless image is scalable vector.
420 if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
421 $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
422 $handlerParams['width'] = $prefWidth;
423 }
424 }
425 }
426
427 // Parser::makeImage has a similarly named variable
428 $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
429 isset( $frameParams['manualthumb'] ) ||
430 isset( $frameParams['framed'] );
431
432 if ( $hasVisibleCaption ) {
433 if ( $enableLegacyMediaDOM ) {
434 // This is no longer needed in our new media output, since the
435 // default styling in content.media-common.less takes care of it;
436 // see T269704.
437
438 # Create a thumbnail. Alignment depends on the writing direction of
439 # the page content language (right-aligned for LTR languages,
440 # left-aligned for RTL languages)
441 # If a thumbnail width has not been provided, it is set
442 # to the default user option as specified in Language*.php
443 if ( $frameParams['align'] == '' ) {
444 $frameParams['align'] = $parser->getTargetLanguage()->alignEnd();
445 }
446 }
447 return $prefix . self::makeThumbLink2(
448 $title, $file, $frameParams, $handlerParams, $time, $query,
449 $classes, $parser
450 ) . $postfix;
451 }
452
453 $rdfaType = 'mw:File';
454
455 if ( isset( $frameParams['frameless'] ) ) {
456 $rdfaType .= '/Frameless';
457 if ( $file ) {
458 $srcWidth = $file->getWidth( $page );
459 # For "frameless" option: do not present an image bigger than the
460 # source (for bitmap-style images). This is the same behavior as the
461 # "thumb" option does it already.
462 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
463 $handlerParams['width'] = $srcWidth;
464 }
465 }
466 }
467
468 if ( $file && isset( $handlerParams['width'] ) ) {
469 # Create a resized image, without the additional thumbnail features
470 $thumb = $file->transform( $handlerParams );
471 } else {
472 $thumb = false;
473 }
474
475 $isBadFile = $file && $thumb &&
476 $parser->getBadFileLookup()->isBadFile( $title->getDBkey(), $parser->getTitle() );
477
478 if ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
479 $rdfaType = 'mw:Error ' . $rdfaType;
480 $currentExists = $file && $file->exists();
481 if ( $enableLegacyMediaDOM ) {
482 $label = $frameParams['title'];
483 } else {
484 if ( $currentExists && !$thumb ) {
485 $label = wfMessage( 'thumbnail_error', '' )->text();
486 } elseif ( $thumb && $thumb->isError() ) {
487 Assert::invariant(
488 $thumb instanceof MediaTransformError,
489 'Unknown MediaTransformOutput: ' . get_class( $thumb )
490 );
491 $label = $thumb->toText();
492 } else {
493 $label = $frameParams['alt'] ?? '';
494 }
495 }
497 $title, $label, '', '', '', (bool)$time, $handlerParams, $currentExists
498 );
499 } else {
500 self::processResponsiveImages( $file, $thumb, $handlerParams );
501 $params = [];
502 // An empty alt indicates an image is not a key part of the content
503 // and that non-visual browsers may omit it from rendering. Only
504 // set the parameter if it's explicitly requested.
505 if ( isset( $frameParams['alt'] ) ) {
506 $params['alt'] = $frameParams['alt'];
507 }
508 $params['title'] = $frameParams['title'];
509 if ( $enableLegacyMediaDOM ) {
510 $params += [
511 'valign' => $frameParams['valign'] ?? false,
512 'img-class' => $frameParams['class'],
513 ];
514 if ( isset( $frameParams['border'] ) ) {
515 $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
516 }
517 } else {
518 $params += [
519 'img-class' => 'mw-file-element',
520 ];
521 }
522 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
523 $s = $thumb->toHtml( $params );
524 }
525
526 if ( $enableLegacyMediaDOM ) {
527 if ( $frameParams['align'] != '' ) {
528 $s = Html::rawElement(
529 'div',
530 [ 'class' => 'float' . $frameParams['align'] ],
531 $s
532 );
533 }
534 return str_replace( "\n", ' ', $prefix . $s . $postfix );
535 }
536
537 $wrapper = 'span';
538 $caption = '';
539
540 if ( $frameParams['align'] != '' ) {
541 $wrapper = 'figure';
542 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
543 $classes[] = "mw-halign-{$frameParams['align']}";
544 $caption = Html::rawElement(
545 'figcaption', [], $frameParams['caption'] ?? ''
546 );
547 } elseif ( isset( $frameParams['valign'] ) ) {
548 // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
549 // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
550 // mw-valign-text-bottom
551 $classes[] = "mw-valign-{$frameParams['valign']}";
552 }
553
554 if ( isset( $frameParams['border'] ) ) {
555 $classes[] = 'mw-image-border';
556 }
557
558 if ( isset( $frameParams['class'] ) ) {
559 $classes[] = $frameParams['class'];
560 }
561
562 $attribs = [
563 'class' => $classes,
564 'typeof' => $rdfaType,
565 ];
566
567 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
568
569 return str_replace( "\n", ' ', $s );
570 }
571
580 public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
581 $mtoParams = [];
582 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
583 $mtoParams['custom-url-link'] = $frameParams['link-url'];
584 if ( isset( $frameParams['link-target'] ) ) {
585 $mtoParams['custom-target-link'] = $frameParams['link-target'];
586 }
587 if ( $parser ) {
588 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
589 foreach ( $extLinkAttrs as $name => $val ) {
590 // Currently could include 'rel' and 'target'
591 $mtoParams['parser-extlink-' . $name] = $val;
592 }
593 }
594 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
595 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
596 $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
597 $linkRenderer->normalizeTarget( $frameParams['link-title'] )
598 );
599 if ( isset( $frameParams['link-title-query'] ) ) {
600 $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
601 }
602 } elseif ( !empty( $frameParams['no-link'] ) ) {
603 // No link
604 } else {
605 $mtoParams['desc-link'] = true;
606 $mtoParams['desc-query'] = $query;
607 }
608 return $mtoParams;
609 }
610
623 public static function makeThumbLinkObj(
624 LinkTarget $title, $file, $label = '', $alt = '', $align = null,
625 $params = [], $framed = false, $manualthumb = ''
626 ) {
627 $frameParams = [
628 'alt' => $alt,
629 'caption' => $label,
630 'align' => $align
631 ];
632 $classes = [];
633 if ( $manualthumb ) {
634 $frameParams['manualthumb'] = $manualthumb;
635 } elseif ( $framed ) {
636 $frameParams['framed'] = true;
637 } elseif ( !isset( $params['width'] ) ) {
638 $classes[] = 'mw-default-size';
639 }
641 $title, $file, $frameParams, $params, false, '', $classes
642 );
643 }
644
656 public static function makeThumbLink2(
657 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
658 $time = false, $query = '', array $classes = [], ?Parser $parser = null
659 ) {
660 $exists = $file && $file->exists();
661
662 $services = MediaWikiServices::getInstance();
663 $enableLegacyMediaDOM = $services->getMainConfig()->get( MainConfigNames::ParserEnableLegacyMediaDOM );
664
665 $page = $handlerParams['page'] ?? false;
666 $lang = $handlerParams['lang'] ?? false;
667
668 if ( !isset( $frameParams['align'] ) ) {
669 $frameParams['align'] = '';
670 if ( $enableLegacyMediaDOM ) {
671 $frameParams['align'] = 'right';
672 }
673 }
674 if ( !isset( $frameParams['caption'] ) ) {
675 $frameParams['caption'] = '';
676 }
677
678 if ( empty( $handlerParams['width'] ) ) {
679 // Reduce width for upright images when parameter 'upright' is used
680 $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
681 }
682
683 $thumb = false;
684 $noscale = false;
685 $manualthumb = false;
686 $manual_title = '';
687 $rdfaType = 'mw:File/Thumb';
688
689 if ( !$exists ) {
690 // Same precedence as the $exists case
691 if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
692 $rdfaType = 'mw:File/Frame';
693 }
694 $outerWidth = $handlerParams['width'] + 2;
695 } else {
696 if ( isset( $frameParams['manualthumb'] ) ) {
697 # Use manually specified thumbnail
698 $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
699 if ( $manual_title ) {
700 $manual_img = $services->getRepoGroup()
701 ->findFile( $manual_title );
702 if ( $manual_img ) {
703 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
704 $manualthumb = true;
705 }
706 }
707 } else {
708 $srcWidth = $file->getWidth( $page );
709 if ( isset( $frameParams['framed'] ) ) {
710 $rdfaType = 'mw:File/Frame';
711 if ( !$file->isVectorized() ) {
712 // Use image dimensions, don't scale
713 $noscale = true;
714 } else {
715 // framed is unscaled, but for vectorized images
716 // we need to a width for scaling up for the high density variants
717 $handlerParams['width'] = $srcWidth;
718 }
719 }
720
721 // Do not present an image bigger than the source, for bitmap-style images
722 // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
723 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
724 $handlerParams['width'] = $srcWidth;
725 }
726
727 $thumb = $noscale
728 ? $file->getUnscaledThumb( $handlerParams )
729 : $file->transform( $handlerParams );
730 }
731
732 if ( $thumb ) {
733 $outerWidth = $thumb->getWidth() + 2;
734 } else {
735 $outerWidth = $handlerParams['width'] + 2;
736 }
737 }
738
739 if ( !$enableLegacyMediaDOM && $parser && $rdfaType === 'mw:File/Thumb' ) {
740 $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
741 }
742
743 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
744 $linkTitleQuery = [];
745 if ( $page || $lang ) {
746 if ( $page ) {
747 $linkTitleQuery['page'] = $page;
748 }
749 if ( $lang ) {
750 $linkTitleQuery['lang'] = $lang;
751 }
752 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
753 # So we don't need to pass it here in $query. However, the URL for the
754 # zoom icon still needs it, so we make a unique query for it. See T16771
755 $url = wfAppendQuery( $url, $linkTitleQuery );
756 }
757
758 if ( $manualthumb
759 && !isset( $frameParams['link-title'] )
760 && !isset( $frameParams['link-url'] )
761 && !isset( $frameParams['no-link'] ) ) {
762 $frameParams['link-title'] = $title;
763 $frameParams['link-title-query'] = $linkTitleQuery;
764 }
765
766 if ( $frameParams['align'] != '' ) {
767 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
768 $classes[] = "mw-halign-{$frameParams['align']}";
769 }
770
771 if ( isset( $frameParams['class'] ) ) {
772 $classes[] = $frameParams['class'];
773 }
774
775 $s = '';
776
777 if ( $enableLegacyMediaDOM ) {
778 $s .= "<div class=\"thumb t{$frameParams['align']}\">"
779 . "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
780 }
781
782 $isBadFile = $exists && $thumb && $parser &&
783 $parser->getBadFileLookup()->isBadFile(
784 $manualthumb ? $manual_title : $title->getDBkey(),
785 $parser->getTitle()
786 );
787
788 if ( !$exists ) {
789 $rdfaType = 'mw:Error ' . $rdfaType;
790 $label = '';
791 if ( !$enableLegacyMediaDOM ) {
792 $label = $frameParams['alt'] ?? '';
793 }
795 $title, $label, '', '', '', (bool)$time, $handlerParams, false
796 );
797 $zoomIcon = '';
798 } elseif ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
799 $rdfaType = 'mw:Error ' . $rdfaType;
800 if ( $enableLegacyMediaDOM ) {
801 if ( !$thumb ) {
802 $s .= wfMessage( 'thumbnail_error', '' )->escaped();
803 } else {
805 $title, '', '', '', '', (bool)$time, $handlerParams, true
806 );
807 }
808 } else {
809 if ( $thumb && $thumb->isError() ) {
810 Assert::invariant(
811 $thumb instanceof MediaTransformError,
812 'Unknown MediaTransformOutput: ' . get_class( $thumb )
813 );
814 $label = $thumb->toText();
815 } elseif ( !$thumb ) {
816 $label = wfMessage( 'thumbnail_error', '' )->text();
817 } else {
818 $label = '';
819 }
821 $title, $label, '', '', '', (bool)$time, $handlerParams, true
822 );
823 }
824 $zoomIcon = '';
825 } else {
826 if ( !$noscale && !$manualthumb ) {
827 self::processResponsiveImages( $file, $thumb, $handlerParams );
828 }
829 $params = [];
830 // An empty alt indicates an image is not a key part of the content
831 // and that non-visual browsers may omit it from rendering. Only
832 // set the parameter if it's explicitly requested.
833 if ( isset( $frameParams['alt'] ) ) {
834 $params['alt'] = $frameParams['alt'];
835 }
836 if ( $enableLegacyMediaDOM ) {
837 $params += [
838 'img-class' => ( isset( $frameParams['class'] ) && $frameParams['class'] !== ''
839 ? $frameParams['class'] . ' '
840 : '' ) . 'thumbimage'
841 ];
842 } else {
843 $params += [
844 'img-class' => 'mw-file-element',
845 ];
846 // Only thumbs gets the magnify link
847 if ( $rdfaType === 'mw:File/Thumb' ) {
848 $params['magnify-resource'] = $url;
849 }
850 }
851 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
852 $s .= $thumb->toHtml( $params );
853 if ( isset( $frameParams['framed'] ) ) {
854 $zoomIcon = '';
855 } else {
856 $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
857 Html::rawElement( 'a', [
858 'href' => $url,
859 'class' => 'internal',
860 'title' => wfMessage( 'thumbnail-more' )->text(),
861 ] )
862 );
863 }
864 }
865
866 if ( $enableLegacyMediaDOM ) {
867 $s .= ' <div class="thumbcaption">' . $zoomIcon . $frameParams['caption'] . '</div></div></div>';
868 return str_replace( "\n", ' ', $s );
869 }
870
871 $s .= Html::rawElement(
872 'figcaption', [], $frameParams['caption'] ?? ''
873 );
874
875 $attribs = [
876 'class' => $classes,
877 'typeof' => $rdfaType,
878 ];
879
880 $s = Html::rawElement( 'figure', $attribs, $s );
881
882 return str_replace( "\n", ' ', $s );
883 }
884
893 public static function processResponsiveImages( $file, $thumb, $hp ) {
894 $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
895 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
896 $hp15 = $hp;
897 $hp15['width'] = round( $hp['width'] * 1.5 );
898 $hp20 = $hp;
899 $hp20['width'] = $hp['width'] * 2;
900 if ( isset( $hp['height'] ) ) {
901 $hp15['height'] = round( $hp['height'] * 1.5 );
902 $hp20['height'] = $hp['height'] * 2;
903 }
904
905 $thumb15 = $file->transform( $hp15 );
906 $thumb20 = $file->transform( $hp20 );
907 if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
908 $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
909 }
910 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
911 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
912 }
913 }
914 }
915
930 public static function makeBrokenImageLinkObj(
931 $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
932 $time = false, array $handlerParams = [], bool $currentExists = false
933 ) {
934 if ( !$title instanceof LinkTarget ) {
935 wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
936 return "<!-- ERROR -->" . htmlspecialchars( $label );
937 }
938
939 $title = Title::newFromLinkTarget( $title );
940 $services = MediaWikiServices::getInstance();
941 $mainConfig = $services->getMainConfig();
942 $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
943 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
944 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
945 if ( $label == '' ) {
946 $label = $title->getPrefixedText();
947 }
948
949 $html = Html::element( 'span', [
950 'class' => 'mw-file-element mw-broken-media',
951 // These data attributes are used to dynamically size the span, see T273013
952 'data-width' => $handlerParams['width'] ?? null,
953 'data-height' => $handlerParams['height'] ?? null,
954 ], $label );
955
956 if ( $mainConfig->get( MainConfigNames::ParserEnableLegacyMediaDOM ) ) {
957 $html = htmlspecialchars( $label, ENT_COMPAT );
958 }
959
960 $repoGroup = $services->getRepoGroup();
961 $currentExists = $currentExists ||
962 ( $time && $repoGroup->findFile( $title ) !== false );
963
964 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
965 && !$currentExists
966 ) {
967 if (
968 $title->inNamespace( NS_FILE ) &&
969 $repoGroup->getLocalRepo()->checkRedirect( $title )
970 ) {
971 // We already know it's a redirect, so mark it accordingly
972 return self::link(
973 $title,
974 $html,
975 [ 'class' => 'mw-redirect' ],
976 wfCgiToArray( $query ),
977 [ 'known', 'noclasses' ]
978 );
979 }
980 return Html::rawElement( 'a', [
981 'href' => self::getUploadUrl( $title, $query ),
982 'class' => 'new',
983 'title' => $title->getPrefixedText()
984 ], $html );
985 }
986 return self::link(
987 $title,
988 $html,
989 [],
990 wfCgiToArray( $query ),
991 [ 'known', 'noclasses' ]
992 );
993 }
994
1003 public static function getUploadUrl( $destFile, $query = '' ) {
1004 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1005 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
1006 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
1007 $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
1008 if ( $query != '' ) {
1009 $q .= '&' . $query;
1010 }
1011
1012 if ( $uploadMissingFileUrl ) {
1013 return wfAppendQuery( $uploadMissingFileUrl, $q );
1014 }
1015
1016 if ( $uploadNavigationUrl ) {
1017 return wfAppendQuery( $uploadNavigationUrl, $q );
1018 }
1019
1020 $upload = SpecialPage::getTitleFor( 'Upload' );
1021
1022 return $upload->getLocalURL( $q );
1023 }
1024
1034 public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
1035 $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
1036 $title, [ 'time' => $time ]
1037 );
1038 return self::makeMediaLinkFile( $title, $img, $html );
1039 }
1040
1053 public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
1054 if ( $file && $file->exists() ) {
1055 $url = $file->getUrl();
1056 $class = 'internal';
1057 } else {
1058 $url = self::getUploadUrl( $title );
1059 $class = 'new';
1060 }
1061
1062 $alt = $title->getText();
1063 if ( $html == '' ) {
1064 $html = $alt;
1065 }
1066
1067 $ret = '';
1068 $attribs = [
1069 'href' => $url,
1070 'class' => $class,
1071 'title' => $alt
1072 ];
1073
1074 if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
1075 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
1076 ) {
1077 wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
1078 . "with url {$url} and text {$html} to {$ret}" );
1079 return $ret;
1080 }
1081
1082 return Html::rawElement( 'a', $attribs, $html );
1083 }
1084
1095 public static function specialLink( $name, $key = '' ) {
1096 $queryPos = strpos( $name, '?' );
1097 if ( $queryPos !== false ) {
1098 $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
1099 $name = substr( $name, 0, $queryPos );
1100 } else {
1101 $getParams = [];
1102 }
1103
1104 $slashPos = strpos( $name, '/' );
1105 if ( $slashPos !== false ) {
1106 $subpage = substr( $name, $slashPos + 1 );
1107 $name = substr( $name, 0, $slashPos );
1108 } else {
1109 $subpage = false;
1110 }
1111
1112 if ( $key == '' ) {
1113 $key = strtolower( $name );
1114 }
1115
1116 return self::linkKnown(
1117 SpecialPage::getTitleFor( $name, $subpage ),
1118 wfMessage( $key )->escaped(),
1119 [],
1120 $getParams
1121 );
1122 }
1123
1144 public static function makeExternalLink( $url, $text, $escape = true,
1145 $linktype = '', $attribs = [], $title = null
1146 ) {
1147 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1148 global $wgTitle;
1149 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1150 return $linkRenderer->makeExternalLink(
1151 $url,
1152 $escape ? $text : new HtmlArmor( $text ),
1153 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1154 $linktype,
1155 $attribs
1156 );
1157 }
1158
1171 public static function userLink(
1172 $userId,
1173 $userName,
1174 $altUserName = false,
1175 $attributes = []
1176 ) {
1177 if ( $userName === '' || $userName === false || $userName === null ) {
1178 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1179 'that need to be fixed?' );
1180 return wfMessage( 'empty-username' )->parse();
1181 }
1182
1183 $classes = 'mw-userlink';
1184 if ( MediaWikiServices::getInstance()->getTempUserConfig()->isTempName( $userName ) ) {
1185 $classes .= ' mw-tempuserlink';
1186 $page = SpecialPage::getTitleValueFor( 'Contributions', $userName );
1187 } elseif ( $userId == 0 ) {
1188 $page = ExternalUserNames::getUserLinkTitle( $userName );
1189
1190 if ( ExternalUserNames::isExternal( $userName ) ) {
1191 $classes .= ' mw-extuserlink';
1192 } elseif ( $altUserName === false ) {
1193 $altUserName = IPUtils::prettifyIP( $userName );
1194 }
1195 $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
1196 } else {
1197 $page = TitleValue::tryNew( NS_USER, strtr( $userName, ' ', '_' ) );
1198 }
1199
1200 // Wrap the output with <bdi> tags for directionality isolation
1201 $linkText =
1202 '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>';
1203
1204 if ( isset( $attributes['class'] ) ) {
1205 $attributes['class'] .= ' ' . $classes;
1206 } else {
1207 $attributes['class'] = $classes;
1208 }
1209
1210 return $page
1211 ? self::link( $page, $linkText, $attributes )
1212 : Html::rawElement( 'span', $attributes, $linkText );
1213 }
1214
1233 public static function userToolLinkArray(
1234 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1235 ): array {
1236 $services = MediaWikiServices::getInstance();
1237 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1238 $talkable = !( $disableAnonTalk && $userId == 0 );
1239 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1240 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1241
1242 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1243 // No tools for an external user
1244 return [];
1245 }
1246
1247 $items = [];
1248 if ( $talkable ) {
1249 $items[] = self::userTalkLink( $userId, $userText );
1250 }
1251 if ( $userId ) {
1252 // check if the user has an edit
1253 $attribs = [];
1254 $attribs['class'] = 'mw-usertoollinks-contribs';
1255 if ( $redContribsWhenNoEdits ) {
1256 if ( $edits === null ) {
1257 $user = UserIdentityValue::newRegistered( $userId, $userText );
1258 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1259 }
1260 if ( $edits === 0 ) {
1261 // Note: "new" class is inappropriate here, as "new" class
1262 // should only be used for pages that do not exist.
1263 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1264 }
1265 }
1266 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1267
1268 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1269 }
1270 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1271 if ( $blockable && $userCanBlock ) {
1272 $items[] = self::blockLink( $userId, $userText );
1273 }
1274
1275 if (
1276 $addEmailLink
1277 && MediaWikiServices::getInstance()->getEmailUserFactory()
1278 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1279 ->canSend()
1280 ->isGood()
1281 ) {
1282 $items[] = self::emailLink( $userId, $userText );
1283 }
1284
1285 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1286
1287 return $items;
1288 }
1289
1297 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1298 global $wgLang;
1299
1300 if ( !$items ) {
1301 return '';
1302 }
1303
1304 if ( $useParentheses ) {
1305 return wfMessage( 'word-separator' )->escaped()
1306 . '<span class="mw-usertoollinks">'
1307 . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1308 . '</span>';
1309 }
1310
1311 $tools = [];
1312 foreach ( $items as $tool ) {
1313 $tools[] = Html::rawElement( 'span', [], $tool );
1314 }
1315 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1316 implode( ' ', $tools ) . '</span>';
1317 }
1318
1333 public static function userToolLinks(
1334 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1335 $useParentheses = true
1336 ) {
1337 if ( $userText === '' ) {
1338 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1339 'that need to be fixed?' );
1340 return ' ' . wfMessage( 'empty-username' )->parse();
1341 }
1342
1343 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1344 return self::renderUserToolLinksArray( $items, $useParentheses );
1345 }
1346
1356 public static function userToolLinksRedContribs(
1357 $userId, $userText, $edits = null, $useParentheses = true
1358 ) {
1359 return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1360 }
1361
1368 public static function userTalkLink( $userId, $userText ) {
1369 if ( $userText === '' ) {
1370 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1371 'that need to be fixed?' );
1372 return wfMessage( 'empty-username' )->parse();
1373 }
1374
1375 $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1376 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1377 $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1378
1379 return $userTalkPage
1380 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1381 : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1382 }
1383
1390 public static function blockLink( $userId, $userText ) {
1391 if ( $userText === '' ) {
1392 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1393 'that need to be fixed?' );
1394 return wfMessage( 'empty-username' )->parse();
1395 }
1396
1397 $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1398 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1399
1400 return self::link( $blockPage,
1401 wfMessage( 'blocklink' )->escaped(),
1402 $moreLinkAttribs
1403 );
1404 }
1405
1411 public static function emailLink( $userId, $userText ) {
1412 if ( $userText === '' ) {
1413 wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1414 'that need to be fixed?' );
1415 return wfMessage( 'empty-username' )->parse();
1416 }
1417
1418 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1419 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1420 return self::link( $emailPage,
1421 wfMessage( 'emaillink' )->escaped(),
1422 $moreLinkAttribs
1423 );
1424 }
1425
1437 public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1438 // TODO inject authority
1439 $authority = RequestContext::getMain()->getAuthority();
1440
1441 $revUser = $revRecord->getUser(
1442 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1443 $authority
1444 );
1445 if ( $revUser ) {
1446 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1447 } else {
1448 // User is deleted and we can't (or don't want to) view it
1449 $link = wfMessage( 'rev-deleted-user' )->escaped();
1450 }
1451
1452 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1453 $class = self::getRevisionDeletedClass( $revRecord );
1454 return '<span class="' . $class . '">' . $link . '</span>';
1455 }
1456 return $link;
1457 }
1458
1465 public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1466 $class = 'history-deleted';
1467 if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1468 $class .= ' mw-history-suppressed';
1469 }
1470 return $class;
1471 }
1472
1485 public static function revUserTools(
1486 RevisionRecord $revRecord,
1487 $isPublic = false,
1488 $useParentheses = true
1489 ) {
1490 // TODO inject authority
1491 $authority = RequestContext::getMain()->getAuthority();
1492
1493 $revUser = $revRecord->getUser(
1494 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1495 $authority
1496 );
1497 if ( $revUser ) {
1498 $link = self::userLink(
1499 $revUser->getId(),
1500 $revUser->getName(),
1501 false,
1502 [ 'data-mw-revid' => $revRecord->getId() ]
1503 ) . self::userToolLinks(
1504 $revUser->getId(),
1505 $revUser->getName(),
1506 false,
1507 0,
1508 null,
1509 $useParentheses
1510 );
1511 } else {
1512 // User is deleted and we can't (or don't want to) view it
1513 $link = wfMessage( 'rev-deleted-user' )->escaped();
1514 }
1515
1516 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1517 $class = self::getRevisionDeletedClass( $revRecord );
1518 return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1519 }
1520 return $link;
1521 }
1522
1533 public static function expandLocalLinks( string $html ) {
1534 return HtmlHelper::modifyElements(
1535 $html,
1536 static function ( SerializerNode $node ): bool {
1537 return $node->name === 'a' && isset( $node->attrs['href'] );
1538 },
1539 static function ( SerializerNode $node ): SerializerNode {
1540 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1541 $node->attrs['href'] =
1542 $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE ) ?? false;
1543 return $node;
1544 }
1545 );
1546 }
1547
1554 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1555 # Valid link forms:
1556 # Foobar -- normal
1557 # :Foobar -- override special treatment of prefix (images, language links)
1558 # /Foobar -- convert to CurrentPage/Foobar
1559 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1560 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1561 # ../Foobar -- convert to CurrentPage/Foobar,
1562 # (from CurrentPage/CurrentSubPage)
1563 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1564 # (from CurrentPage/CurrentSubPage)
1565
1566 $ret = $target; # default return value is no change
1567
1568 # Some namespaces don't allow subpages,
1569 # so only perform processing if subpages are allowed
1570 if (
1571 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1572 hasSubpages( $contextTitle->getNamespace() )
1573 ) {
1574 $hash = strpos( $target, '#' );
1575 if ( $hash !== false ) {
1576 $suffix = substr( $target, $hash );
1577 $target = substr( $target, 0, $hash );
1578 } else {
1579 $suffix = '';
1580 }
1581 # T9425
1582 $target = trim( $target );
1583 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1584 getPrefixedText( $contextTitle );
1585 # Look at the first character
1586 if ( $target != '' && $target[0] === '/' ) {
1587 # / at end means we don't want the slash to be shown
1588 $m = [];
1589 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1590 if ( $trailingSlashes ) {
1591 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1592 } else {
1593 $noslash = substr( $target, 1 );
1594 }
1595
1596 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1597 if ( $text === '' ) {
1598 $text = $target . $suffix;
1599 } # this might be changed for ugliness reasons
1600 } else {
1601 # check for .. subpage backlinks
1602 $dotdotcount = 0;
1603 $nodotdot = $target;
1604 while ( str_starts_with( $nodotdot, '../' ) ) {
1605 ++$dotdotcount;
1606 $nodotdot = substr( $nodotdot, 3 );
1607 }
1608 if ( $dotdotcount > 0 ) {
1609 $exploded = explode( '/', $contextPrefixedText );
1610 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1611 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1612 # / at the end means don't show full path
1613 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1614 $nodotdot = rtrim( $nodotdot, '/' );
1615 if ( $text === '' ) {
1616 $text = $nodotdot . $suffix;
1617 }
1618 }
1619 $nodotdot = trim( $nodotdot );
1620 if ( $nodotdot != '' ) {
1621 $ret .= '/' . $nodotdot;
1622 }
1623 $ret .= $suffix;
1624 }
1625 }
1626 }
1627 }
1628
1629 return $ret;
1630 }
1631
1637 public static function formatRevisionSize( $size ) {
1638 if ( $size == 0 ) {
1639 $stxt = wfMessage( 'historyempty' )->escaped();
1640 } else {
1641 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1642 }
1643 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1644 }
1645
1652 public static function splitTrail( $trail ) {
1653 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1654 $inside = '';
1655 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1656 [ , $inside, $trail ] = $m;
1657 }
1658 return [ $inside, $trail ];
1659 }
1660
1691 public static function generateRollback(
1692 RevisionRecord $revRecord,
1693 ?IContextSource $context = null,
1694 $options = []
1695 ) {
1696 $context ??= RequestContext::getMain();
1697
1698 $editCount = self::getRollbackEditCount( $revRecord );
1699 if ( $editCount === false ) {
1700 return '';
1701 }
1702
1703 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1704
1705 $services = MediaWikiServices::getInstance();
1706 // Allow extensions to modify the rollback link.
1707 // Abort further execution if the extension wants full control over the link.
1708 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1709 $revRecord, $context, $options, $inner ) ) {
1710 return $inner;
1711 }
1712
1713 if ( !in_array( 'noBrackets', $options, true ) ) {
1714 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1715 }
1716
1717 if ( $services->getUserOptionsLookup()
1718 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1719 ) {
1720 $services->getStatsFactory()
1721 ->getCounter( 'rollbackconfirmation_event_load_total' )
1722 ->copyToStatsdAt( 'rollbackconfirmation.event.load' )
1723 ->increment();
1724 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1725 }
1726
1727 return '<span class="mw-rollback-link">' . $inner . '</span>';
1728 }
1729
1748 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1749 if ( func_num_args() > 1 ) {
1750 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1751 }
1752 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1753 ->get( MainConfigNames::ShowRollbackEditCount );
1754
1755 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1756 // Nothing has happened, indicate this by returning 'null'
1757 return null;
1758 }
1759
1760 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1761
1762 // Up to the value of $wgShowRollbackEditCount revisions are counted
1763 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1764 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1765 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1766 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1767 ->limit( $showRollbackEditCount + 1 )
1768 ->caller( __METHOD__ )->fetchResultSet();
1769
1770 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1771 $revUserText = $revUser ? $revUser->getName() : '';
1772
1773 $editCount = 0;
1774 $moreRevs = false;
1775 foreach ( $res as $row ) {
1776 if ( $row->rev_user_text != $revUserText ) {
1777 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1778 || $row->rev_deleted & RevisionRecord::DELETED_USER
1779 ) {
1780 // If the user or the text of the revision we might rollback
1781 // to is deleted in some way we can't rollback. Similar to
1782 // the checks in WikiPage::commitRollback.
1783 return false;
1784 }
1785 $moreRevs = true;
1786 break;
1787 }
1788 $editCount++;
1789 }
1790
1791 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1792 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1793 // and there weren't any other revisions. That means that the current user is the only
1794 // editor, so we can't rollback
1795 return false;
1796 }
1797 return $editCount;
1798 }
1799
1814 public static function buildRollbackLink(
1815 RevisionRecord $revRecord,
1816 ?IContextSource $context = null,
1817 $editCount = false
1818 ) {
1819 $config = MediaWikiServices::getInstance()->getMainConfig();
1820 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1821 $miserMode = $config->get( MainConfigNames::MiserMode );
1822 // To config which pages are affected by miser mode
1823 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1824
1825 $context ??= RequestContext::getMain();
1826
1827 $title = $revRecord->getPageAsLinkTarget();
1828 $revUser = $revRecord->getUser();
1829 $revUserText = $revUser ? $revUser->getName() : '';
1830
1831 $query = [
1832 'action' => 'rollback',
1833 'from' => $revUserText,
1834 'token' => $context->getUser()->getEditToken( 'rollback' ),
1835 ];
1836
1837 $attrs = [
1838 'data-mw' => 'interface',
1839 'title' => $context->msg( 'tooltip-rollback' )->text()
1840 ];
1841
1842 $options = [ 'known', 'noclasses' ];
1843
1844 if ( $context->getRequest()->getBool( 'bot' ) ) {
1845 // T17999
1846 $query['hidediff'] = '1';
1847 $query['bot'] = '1';
1848 }
1849
1850 if ( $miserMode ) {
1851 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1852 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1853 $showRollbackEditCount = false;
1854 break;
1855 }
1856 }
1857 }
1858
1859 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1860 $msg = [ 'rollbacklink' ];
1861 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1862 if ( !is_numeric( $editCount ) ) {
1863 $editCount = self::getRollbackEditCount( $revRecord );
1864 }
1865
1866 if ( $editCount > $showRollbackEditCount ) {
1867 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1868 } elseif ( $editCount ) {
1869 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1870 }
1871 }
1872
1873 $html = $context->msg( ...$msg )->parse();
1874 return self::link( $title, $html, $attrs, $query, $options );
1875 }
1876
1885 public static function formatHiddenCategories( $hiddencats ) {
1886 $outText = '';
1887 if ( count( $hiddencats ) > 0 ) {
1888 # Construct the HTML
1889 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1890 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1891 $outText .= "</div><ul>\n";
1892
1893 foreach ( $hiddencats as $titleObj ) {
1894 # If it's hidden, it must exist - no need to check with a LinkBatch
1895 $outText .= '<li>'
1896 . self::link( $titleObj, null, [], [], 'known' )
1897 . "</li>\n";
1898 }
1899 $outText .= '</ul>';
1900 }
1901 return $outText;
1902 }
1903
1907 private static function getContextFromMain() {
1908 $context = RequestContext::getMain();
1909 $context = new DerivativeContext( $context );
1910 return $context;
1911 }
1912
1930 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1931 if ( !$localizer ) {
1932 $localizer = self::getContextFromMain();
1933 }
1934 $message = $localizer->msg( "tooltip-$name", $msgParams );
1935 // Set a default tooltip for subject namespace tabs if that hasn't
1936 // been defined. See T22126
1937 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1938 $message = $localizer->msg( 'tooltip-ca-nstab' );
1939 }
1940
1941 if ( $message->isDisabled() ) {
1942 $tooltip = false;
1943 } else {
1944 $tooltip = $message->text();
1945 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1946 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1947 }
1948
1949 $options = (array)$options;
1950
1951 if ( in_array( 'nonexisting', $options ) ) {
1952 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1953 }
1954 if ( in_array( 'withaccess', $options ) ) {
1955 $accesskey = self::accesskey( $name, $localizer );
1956 if ( $accesskey !== false ) {
1957 // Should be build the same as in jquery.accessKeyLabel.js
1958 if ( $tooltip === false || $tooltip === '' ) {
1959 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1960 } else {
1961 $tooltip .= $localizer->msg( 'word-separator' )->text();
1962 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1963 }
1964 }
1965 }
1966
1967 return $tooltip;
1968 }
1969
1971 public static $accesskeycache;
1972
1985 public static function accesskey( $name, $localizer = null ) {
1986 if ( !isset( self::$accesskeycache[$name] ) ) {
1987 if ( !$localizer ) {
1988 $localizer = self::getContextFromMain();
1989 }
1990 $msg = $localizer->msg( "accesskey-$name" );
1991 // Set a default accesskey for subject namespace tabs if an
1992 // accesskey has not been defined. See T22126
1993 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1994 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1995 }
1996 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1997 }
1998 return self::$accesskeycache[$name];
1999 }
2000
2015 public static function getRevDeleteLink(
2016 Authority $performer,
2017 RevisionRecord $revRecord,
2018 LinkTarget $title
2019 ) {
2020 $canHide = $performer->isAllowed( 'deleterevision' );
2021 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
2022 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
2023 return '';
2024 }
2025
2026 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2027 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2028 }
2029 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2030 getPrefixedDBkey( $title );
2031 if ( $revRecord->getId() ) {
2032 // RevDelete links using revision ID are stable across
2033 // page deletion and undeletion; use when possible.
2034 $query = [
2035 'type' => 'revision',
2036 'target' => $prefixedDbKey,
2037 'ids' => $revRecord->getId()
2038 ];
2039 } else {
2040 // Older deleted entries didn't save a revision ID.
2041 // We have to refer to these by timestamp, ick!
2042 $query = [
2043 'type' => 'archive',
2044 'target' => $prefixedDbKey,
2045 'ids' => $revRecord->getTimestamp()
2046 ];
2047 }
2048 return self::revDeleteLink(
2049 $query,
2050 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2051 $canHide
2052 );
2053 }
2054
2067 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2068 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2069 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2070 $html = wfMessage( $msgKey )->escaped();
2071 $tag = $restricted ? 'strong' : 'span';
2072 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2073 return Xml::tags(
2074 $tag,
2075 [ 'class' => 'mw-revdelundel-link' ],
2076 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2077 );
2078 }
2079
2091 public static function revDeleteLinkDisabled( $delete = true ) {
2092 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2093 $html = wfMessage( $msgKey )->escaped();
2094 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2095 return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2096 }
2097
2111 public static function tooltipAndAccesskeyAttribs(
2112 $name,
2113 array $msgParams = [],
2114 $options = null,
2115 $localizer = null
2116 ) {
2117 $options = (array)$options;
2118 $options[] = 'withaccess';
2119
2120 // Get optional parameters from global context if any missing.
2121 if ( !$localizer ) {
2122 $localizer = self::getContextFromMain();
2123 }
2124
2125 $attribs = [
2126 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2127 'accesskey' => self::accesskey( $name, $localizer )
2128 ];
2129 if ( $attribs['title'] === false ) {
2130 unset( $attribs['title'] );
2131 }
2132 if ( $attribs['accesskey'] === false ) {
2133 unset( $attribs['accesskey'] );
2134 }
2135 return $attribs;
2136 }
2137
2145 public static function tooltip( $name, $options = null ) {
2146 $tooltip = self::titleAttrib( $name, $options );
2147 if ( $tooltip === false ) {
2148 return '';
2149 }
2150 return Xml::expandAttributes( [
2151 'title' => $tooltip
2152 ] );
2153 }
2154
2155}
2156
2158class_alias( Linker::class, 'Linker' );
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
const PROTO_RELATIVE
Definition Defines.php:206
const NS_USER_TALK
Definition Defines.php:68
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.
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(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:541
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:541
array $params
The job parameters.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:79
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Basic media transform error class.
Base class for the output of MediaHandler::doTransform() and File::transform().
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.
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:56
Some internal bits split of from Skin.php.
Definition Linker.php:63
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition Linker.php:1533
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2067
static link( $target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition Linker.php:111
static string false[] $accesskeycache
Definition Linker.php:1971
static blockLink( $userId, $userText)
Definition Linker.php:1390
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='', $hash='')
Make appropriate markup for a link to the current article.
Definition Linker.php:193
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null, $localizer=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2111
static getUploadUrl( $destFile, $query='')
Get the URL to upload a certain file.
Definition Linker.php:1003
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:327
static makeMediaLinkObj( $title, $html='', $time=false)
Create a direct link to a given uploaded file.
Definition Linker.php:1034
static processResponsiveImages( $file, $thumb, $hp)
Process responsive images: add 1.5x and 2x subimages to the thumbnail, where applicable.
Definition Linker.php:893
static userTalkLink( $userId, $userText)
Definition Linker.php:1368
static generateRollback(RevisionRecord $revRecord, ?IContextSource $context=null, $options=[])
Generate a rollback link for a given revision.
Definition Linker.php:1691
static buildRollbackLink(RevisionRecord $revRecord, ?IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1814
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1554
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:1095
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:1333
static emailLink( $userId, $userText)
Definition Linker.php:1411
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1885
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition Linker.php:226
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:1748
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1144
static userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1233
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition Linker.php:580
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition Linker.php:169
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:2015
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query='', array $classes=[], ?Parser $parser=null)
Definition Linker.php:656
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:269
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition Linker.php:2145
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[], bool $currentExists=false)
Make a "broken" link to an image.
Definition Linker.php:930
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:1053
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1985
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:1930
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1297
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition Linker.php:1356
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:1652
const TOOL_LINKS_NOBLOCK
Flags for userToolLinks()
Definition Linker.php:67
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2091
static formatRevisionSize( $size)
Definition Linker.php:1637
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:623
static userLink( $userId, $userName, $altUserName=false, $attributes=[])
Make user link (or user contributions for unregistered users)
Definition Linker.php:1171
static revUserLink(RevisionRecord $revRecord, $isPublic=false)
Generate a user link if the current user is allowed to view it.
Definition Linker.php:1437
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1465
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:1485
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 ParserEnableLegacyMediaDOM
Name constant for the ParserEnableLegacyMediaDOM 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.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:155
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:143
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1248
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1174
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.
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,...
static getTitleValueFor( $name, $subpage=false, $fragment='')
Get a localised TitleValue object for a specified special page name.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:78
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.
Module of static functions for generating XML.
Definition Xml.php:37
Build SELECT queries with a fluent interface.
Interface for objects which can provide a MediaWiki context on request.
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:37
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
element(SerializerNode $parent, SerializerNode $node, $contents)