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 global $wgTitle;
1148 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1149 return $linkRenderer->makeExternalLink(
1150 $url,
1151 $escape ? $text : new HtmlArmor( $text ),
1152 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1153 $linktype,
1154 $attribs
1155 );
1156 }
1157
1170 public static function userLink(
1171 $userId,
1172 $userName,
1173 $altUserName = false,
1174 $attributes = []
1175 ) {
1176 if ( $userName === '' || $userName === false || $userName === null ) {
1177 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1178 'that need to be fixed?' );
1179 return wfMessage( 'empty-username' )->parse();
1180 }
1181
1182 $classes = 'mw-userlink';
1183 if ( MediaWikiServices::getInstance()->getTempUserConfig()->isTempName( $userName ) ) {
1184 $classes .= ' mw-tempuserlink';
1185 $page = SpecialPage::getTitleValueFor( 'Contributions', $userName );
1186 } elseif ( $userId == 0 ) {
1187 $page = ExternalUserNames::getUserLinkTitle( $userName );
1188
1189 if ( ExternalUserNames::isExternal( $userName ) ) {
1190 $classes .= ' mw-extuserlink';
1191 } elseif ( $altUserName === false ) {
1192 $altUserName = IPUtils::prettifyIP( $userName );
1193 }
1194 $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
1195 } else {
1196 $page = TitleValue::tryNew( NS_USER, strtr( $userName, ' ', '_' ) );
1197 }
1198
1199 // Wrap the output with <bdi> tags for directionality isolation
1200 $linkText =
1201 '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>';
1202
1203 if ( isset( $attributes['class'] ) ) {
1204 $attributes['class'] .= ' ' . $classes;
1205 } else {
1206 $attributes['class'] = $classes;
1207 }
1208
1209 return $page
1210 ? self::link( $page, $linkText, $attributes )
1211 : Html::rawElement( 'span', $attributes, $linkText );
1212 }
1213
1232 public static function userToolLinkArray(
1233 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1234 ): array {
1235 $services = MediaWikiServices::getInstance();
1236 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1237 $talkable = !( $disableAnonTalk && $userId == 0 );
1238 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1239 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1240
1241 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1242 // No tools for an external user
1243 return [];
1244 }
1245
1246 $items = [];
1247 if ( $talkable ) {
1248 $items[] = self::userTalkLink( $userId, $userText );
1249 }
1250 if ( $userId ) {
1251 // check if the user has an edit
1252 $attribs = [];
1253 $attribs['class'] = 'mw-usertoollinks-contribs';
1254 if ( $redContribsWhenNoEdits ) {
1255 if ( $edits === null ) {
1256 $user = UserIdentityValue::newRegistered( $userId, $userText );
1257 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1258 }
1259 if ( $edits === 0 ) {
1260 // Note: "new" class is inappropriate here, as "new" class
1261 // should only be used for pages that do not exist.
1262 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1263 }
1264 }
1265 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1266
1267 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1268 }
1269 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1270 if ( $blockable && $userCanBlock ) {
1271 $items[] = self::blockLink( $userId, $userText );
1272 }
1273
1274 if (
1275 $addEmailLink
1276 && MediaWikiServices::getInstance()->getEmailUserFactory()
1277 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1278 ->canSend()
1279 ->isGood()
1280 ) {
1281 $items[] = self::emailLink( $userId, $userText );
1282 }
1283
1284 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1285
1286 return $items;
1287 }
1288
1296 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1297 global $wgLang;
1298
1299 if ( !$items ) {
1300 return '';
1301 }
1302
1303 if ( $useParentheses ) {
1304 return wfMessage( 'word-separator' )->escaped()
1305 . '<span class="mw-usertoollinks">'
1306 . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1307 . '</span>';
1308 }
1309
1310 $tools = [];
1311 foreach ( $items as $tool ) {
1312 $tools[] = Html::rawElement( 'span', [], $tool );
1313 }
1314 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1315 implode( ' ', $tools ) . '</span>';
1316 }
1317
1332 public static function userToolLinks(
1333 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1334 $useParentheses = true
1335 ) {
1336 if ( $userText === '' ) {
1337 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1338 'that need to be fixed?' );
1339 return ' ' . wfMessage( 'empty-username' )->parse();
1340 }
1341
1342 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1343 return self::renderUserToolLinksArray( $items, $useParentheses );
1344 }
1345
1355 public static function userToolLinksRedContribs(
1356 $userId, $userText, $edits = null, $useParentheses = true
1357 ) {
1358 return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1359 }
1360
1367 public static function userTalkLink( $userId, $userText ) {
1368 if ( $userText === '' ) {
1369 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1370 'that need to be fixed?' );
1371 return wfMessage( 'empty-username' )->parse();
1372 }
1373
1374 $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1375 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1376 $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1377
1378 return $userTalkPage
1379 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1380 : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1381 }
1382
1389 public static function blockLink( $userId, $userText ) {
1390 if ( $userText === '' ) {
1391 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1392 'that need to be fixed?' );
1393 return wfMessage( 'empty-username' )->parse();
1394 }
1395
1396 $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1397 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1398
1399 return self::link( $blockPage,
1400 wfMessage( 'blocklink' )->escaped(),
1401 $moreLinkAttribs
1402 );
1403 }
1404
1410 public static function emailLink( $userId, $userText ) {
1411 if ( $userText === '' ) {
1412 wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1413 'that need to be fixed?' );
1414 return wfMessage( 'empty-username' )->parse();
1415 }
1416
1417 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1418 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1419 return self::link( $emailPage,
1420 wfMessage( 'emaillink' )->escaped(),
1421 $moreLinkAttribs
1422 );
1423 }
1424
1436 public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1437 // TODO inject authority
1438 $authority = RequestContext::getMain()->getAuthority();
1439
1440 $revUser = $revRecord->getUser(
1441 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1442 $authority
1443 );
1444 if ( $revUser ) {
1445 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1446 } else {
1447 // User is deleted and we can't (or don't want to) view it
1448 $link = wfMessage( 'rev-deleted-user' )->escaped();
1449 }
1450
1451 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1452 $class = self::getRevisionDeletedClass( $revRecord );
1453 return '<span class="' . $class . '">' . $link . '</span>';
1454 }
1455 return $link;
1456 }
1457
1464 public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1465 $class = 'history-deleted';
1466 if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1467 $class .= ' mw-history-suppressed';
1468 }
1469 return $class;
1470 }
1471
1484 public static function revUserTools(
1485 RevisionRecord $revRecord,
1486 $isPublic = false,
1487 $useParentheses = true
1488 ) {
1489 // TODO inject authority
1490 $authority = RequestContext::getMain()->getAuthority();
1491
1492 $revUser = $revRecord->getUser(
1493 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1494 $authority
1495 );
1496 if ( $revUser ) {
1497 $link = self::userLink(
1498 $revUser->getId(),
1499 $revUser->getName(),
1500 false,
1501 [ 'data-mw-revid' => $revRecord->getId() ]
1502 ) . self::userToolLinks(
1503 $revUser->getId(),
1504 $revUser->getName(),
1505 false,
1506 0,
1507 null,
1508 $useParentheses
1509 );
1510 } else {
1511 // User is deleted and we can't (or don't want to) view it
1512 $link = wfMessage( 'rev-deleted-user' )->escaped();
1513 }
1514
1515 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1516 $class = self::getRevisionDeletedClass( $revRecord );
1517 return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1518 }
1519 return $link;
1520 }
1521
1532 public static function expandLocalLinks( string $html ) {
1533 return HtmlHelper::modifyElements(
1534 $html,
1535 static function ( SerializerNode $node ): bool {
1536 return $node->name === 'a' && isset( $node->attrs['href'] );
1537 },
1538 static function ( SerializerNode $node ): SerializerNode {
1539 $node->attrs['href'] =
1540 wfExpandUrl( $node->attrs['href'], PROTO_RELATIVE );
1541 return $node;
1542 }
1543 );
1544 }
1545
1552 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1553 # Valid link forms:
1554 # Foobar -- normal
1555 # :Foobar -- override special treatment of prefix (images, language links)
1556 # /Foobar -- convert to CurrentPage/Foobar
1557 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1558 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1559 # ../Foobar -- convert to CurrentPage/Foobar,
1560 # (from CurrentPage/CurrentSubPage)
1561 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1562 # (from CurrentPage/CurrentSubPage)
1563
1564 $ret = $target; # default return value is no change
1565
1566 # Some namespaces don't allow subpages,
1567 # so only perform processing if subpages are allowed
1568 if (
1569 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1570 hasSubpages( $contextTitle->getNamespace() )
1571 ) {
1572 $hash = strpos( $target, '#' );
1573 if ( $hash !== false ) {
1574 $suffix = substr( $target, $hash );
1575 $target = substr( $target, 0, $hash );
1576 } else {
1577 $suffix = '';
1578 }
1579 # T9425
1580 $target = trim( $target );
1581 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1582 getPrefixedText( $contextTitle );
1583 # Look at the first character
1584 if ( $target != '' && $target[0] === '/' ) {
1585 # / at end means we don't want the slash to be shown
1586 $m = [];
1587 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1588 if ( $trailingSlashes ) {
1589 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1590 } else {
1591 $noslash = substr( $target, 1 );
1592 }
1593
1594 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1595 if ( $text === '' ) {
1596 $text = $target . $suffix;
1597 } # this might be changed for ugliness reasons
1598 } else {
1599 # check for .. subpage backlinks
1600 $dotdotcount = 0;
1601 $nodotdot = $target;
1602 while ( str_starts_with( $nodotdot, '../' ) ) {
1603 ++$dotdotcount;
1604 $nodotdot = substr( $nodotdot, 3 );
1605 }
1606 if ( $dotdotcount > 0 ) {
1607 $exploded = explode( '/', $contextPrefixedText );
1608 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1609 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1610 # / at the end means don't show full path
1611 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1612 $nodotdot = rtrim( $nodotdot, '/' );
1613 if ( $text === '' ) {
1614 $text = $nodotdot . $suffix;
1615 }
1616 }
1617 $nodotdot = trim( $nodotdot );
1618 if ( $nodotdot != '' ) {
1619 $ret .= '/' . $nodotdot;
1620 }
1621 $ret .= $suffix;
1622 }
1623 }
1624 }
1625 }
1626
1627 return $ret;
1628 }
1629
1635 public static function formatRevisionSize( $size ) {
1636 if ( $size == 0 ) {
1637 $stxt = wfMessage( 'historyempty' )->escaped();
1638 } else {
1639 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1640 }
1641 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1642 }
1643
1650 public static function splitTrail( $trail ) {
1651 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1652 $inside = '';
1653 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1654 [ , $inside, $trail ] = $m;
1655 }
1656 return [ $inside, $trail ];
1657 }
1658
1689 public static function generateRollback(
1690 RevisionRecord $revRecord,
1691 IContextSource $context = null,
1692 $options = []
1693 ) {
1694 $context ??= RequestContext::getMain();
1695
1696 $editCount = self::getRollbackEditCount( $revRecord );
1697 if ( $editCount === false ) {
1698 return '';
1699 }
1700
1701 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1702
1703 $services = MediaWikiServices::getInstance();
1704 // Allow extensions to modify the rollback link.
1705 // Abort further execution if the extension wants full control over the link.
1706 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1707 $revRecord, $context, $options, $inner ) ) {
1708 return $inner;
1709 }
1710
1711 if ( !in_array( 'noBrackets', $options, true ) ) {
1712 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1713 }
1714
1715 if ( $services->getUserOptionsLookup()
1716 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1717 ) {
1718 $services->getStatsFactory()
1719 ->getCounter( 'rollbackconfirmation_event_load_total' )
1720 ->copyToStatsdAt( 'rollbackconfirmation.event.load' )
1721 ->increment();
1722 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1723 }
1724
1725 return '<span class="mw-rollback-link">' . $inner . '</span>';
1726 }
1727
1746 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1747 if ( func_num_args() > 1 ) {
1748 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1749 }
1750 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1751 ->get( MainConfigNames::ShowRollbackEditCount );
1752
1753 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1754 // Nothing has happened, indicate this by returning 'null'
1755 return null;
1756 }
1757
1758 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1759
1760 // Up to the value of $wgShowRollbackEditCount revisions are counted
1761 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1762 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1763 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1764 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1765 ->limit( $showRollbackEditCount + 1 )
1766 ->caller( __METHOD__ )->fetchResultSet();
1767
1768 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1769 $revUserText = $revUser ? $revUser->getName() : '';
1770
1771 $editCount = 0;
1772 $moreRevs = false;
1773 foreach ( $res as $row ) {
1774 if ( $row->rev_user_text != $revUserText ) {
1775 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1776 || $row->rev_deleted & RevisionRecord::DELETED_USER
1777 ) {
1778 // If the user or the text of the revision we might rollback
1779 // to is deleted in some way we can't rollback. Similar to
1780 // the checks in WikiPage::commitRollback.
1781 return false;
1782 }
1783 $moreRevs = true;
1784 break;
1785 }
1786 $editCount++;
1787 }
1788
1789 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1790 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1791 // and there weren't any other revisions. That means that the current user is the only
1792 // editor, so we can't rollback
1793 return false;
1794 }
1795 return $editCount;
1796 }
1797
1812 public static function buildRollbackLink(
1813 RevisionRecord $revRecord,
1814 IContextSource $context = null,
1815 $editCount = false
1816 ) {
1817 $config = MediaWikiServices::getInstance()->getMainConfig();
1818 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1819 $miserMode = $config->get( MainConfigNames::MiserMode );
1820 // To config which pages are affected by miser mode
1821 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1822
1823 $context ??= RequestContext::getMain();
1824
1825 $title = $revRecord->getPageAsLinkTarget();
1826 $revUser = $revRecord->getUser();
1827 $revUserText = $revUser ? $revUser->getName() : '';
1828
1829 $query = [
1830 'action' => 'rollback',
1831 'from' => $revUserText,
1832 'token' => $context->getUser()->getEditToken( 'rollback' ),
1833 ];
1834
1835 $attrs = [
1836 'data-mw' => 'interface',
1837 'title' => $context->msg( 'tooltip-rollback' )->text()
1838 ];
1839
1840 $options = [ 'known', 'noclasses' ];
1841
1842 if ( $context->getRequest()->getBool( 'bot' ) ) {
1843 // T17999
1844 $query['hidediff'] = '1';
1845 $query['bot'] = '1';
1846 }
1847
1848 if ( $miserMode ) {
1849 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1850 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1851 $showRollbackEditCount = false;
1852 break;
1853 }
1854 }
1855 }
1856
1857 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1858 $msg = [ 'rollbacklink' ];
1859 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1860 if ( !is_numeric( $editCount ) ) {
1861 $editCount = self::getRollbackEditCount( $revRecord );
1862 }
1863
1864 if ( $editCount > $showRollbackEditCount ) {
1865 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1866 } elseif ( $editCount ) {
1867 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1868 }
1869 }
1870
1871 $html = $context->msg( ...$msg )->parse();
1872 return self::link( $title, $html, $attrs, $query, $options );
1873 }
1874
1883 public static function formatHiddenCategories( $hiddencats ) {
1884 $outText = '';
1885 if ( count( $hiddencats ) > 0 ) {
1886 # Construct the HTML
1887 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1888 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1889 $outText .= "</div><ul>\n";
1890
1891 foreach ( $hiddencats as $titleObj ) {
1892 # If it's hidden, it must exist - no need to check with a LinkBatch
1893 $outText .= '<li>'
1894 . self::link( $titleObj, null, [], [], 'known' )
1895 . "</li>\n";
1896 }
1897 $outText .= '</ul>';
1898 }
1899 return $outText;
1900 }
1901
1905 private static function getContextFromMain() {
1906 $context = RequestContext::getMain();
1907 $context = new DerivativeContext( $context );
1908 return $context;
1909 }
1910
1928 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1929 if ( !$localizer ) {
1930 $localizer = self::getContextFromMain();
1931 }
1932 $message = $localizer->msg( "tooltip-$name", $msgParams );
1933 // Set a default tooltip for subject namespace tabs if that hasn't
1934 // been defined. See T22126
1935 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1936 $message = $localizer->msg( 'tooltip-ca-nstab' );
1937 }
1938
1939 if ( $message->isDisabled() ) {
1940 $tooltip = false;
1941 } else {
1942 $tooltip = $message->text();
1943 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1944 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1945 }
1946
1947 $options = (array)$options;
1948
1949 if ( in_array( 'nonexisting', $options ) ) {
1950 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1951 }
1952 if ( in_array( 'withaccess', $options ) ) {
1953 $accesskey = self::accesskey( $name, $localizer );
1954 if ( $accesskey !== false ) {
1955 // Should be build the same as in jquery.accessKeyLabel.js
1956 if ( $tooltip === false || $tooltip === '' ) {
1957 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1958 } else {
1959 $tooltip .= $localizer->msg( 'word-separator' )->text();
1960 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1961 }
1962 }
1963 }
1964
1965 return $tooltip;
1966 }
1967
1968 public static $accesskeycache;
1969
1982 public static function accesskey( $name, $localizer = null ) {
1983 if ( !isset( self::$accesskeycache[$name] ) ) {
1984 if ( !$localizer ) {
1985 $localizer = self::getContextFromMain();
1986 }
1987 $msg = $localizer->msg( "accesskey-$name" );
1988 // Set a default accesskey for subject namespace tabs if an
1989 // accesskey has not been defined. See T22126
1990 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1991 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1992 }
1993 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1994 }
1995 return self::$accesskeycache[$name];
1996 }
1997
2012 public static function getRevDeleteLink(
2013 Authority $performer,
2014 RevisionRecord $revRecord,
2015 LinkTarget $title
2016 ) {
2017 $canHide = $performer->isAllowed( 'deleterevision' );
2018 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
2019 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
2020 return '';
2021 }
2022
2023 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2024 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2025 }
2026 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2027 getPrefixedDBkey( $title );
2028 if ( $revRecord->getId() ) {
2029 // RevDelete links using revision ID are stable across
2030 // page deletion and undeletion; use when possible.
2031 $query = [
2032 'type' => 'revision',
2033 'target' => $prefixedDbKey,
2034 'ids' => $revRecord->getId()
2035 ];
2036 } else {
2037 // Older deleted entries didn't save a revision ID.
2038 // We have to refer to these by timestamp, ick!
2039 $query = [
2040 'type' => 'archive',
2041 'target' => $prefixedDbKey,
2042 'ids' => $revRecord->getTimestamp()
2043 ];
2044 }
2045 return self::revDeleteLink(
2046 $query,
2047 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2048 $canHide
2049 );
2050 }
2051
2064 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2065 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2066 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2067 $html = wfMessage( $msgKey )->escaped();
2068 $tag = $restricted ? 'strong' : 'span';
2069 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2070 return Xml::tags(
2071 $tag,
2072 [ 'class' => 'mw-revdelundel-link' ],
2073 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2074 );
2075 }
2076
2088 public static function revDeleteLinkDisabled( $delete = true ) {
2089 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2090 $html = wfMessage( $msgKey )->escaped();
2091 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2092 return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2093 }
2094
2108 public static function tooltipAndAccesskeyAttribs(
2109 $name,
2110 array $msgParams = [],
2111 $options = null,
2112 $localizer = null
2113 ) {
2114 $options = (array)$options;
2115 $options[] = 'withaccess';
2116
2117 // Get optional parameters from global context if any missing.
2118 if ( !$localizer ) {
2119 $localizer = self::getContextFromMain();
2120 }
2121
2122 $attribs = [
2123 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2124 'accesskey' => self::accesskey( $name, $localizer )
2125 ];
2126 if ( $attribs['title'] === false ) {
2127 unset( $attribs['title'] );
2128 }
2129 if ( $attribs['accesskey'] === false ) {
2130 unset( $attribs['accesskey'] );
2131 }
2132 return $attribs;
2133 }
2134
2142 public static function tooltip( $name, $options = null ) {
2143 $tooltip = self::titleAttrib( $name, $options );
2144 if ( $tooltip === false ) {
2145 return '';
2146 }
2147 return Xml::expandAttributes( [
2148 'title' => $tooltip
2149 ] );
2150 }
2151
2152}
2153
2155class_alias( Linker::class, 'Linker' );
getAuthority()
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:205
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.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL using $wgServer (or one of its alternatives).
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:538
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:538
array $params
The job parameters.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:74
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:1532
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2064
static link( $target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition Linker.php:111
static blockLink( $userId, $userText)
Definition Linker.php:1389
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:2108
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:1367
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1552
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:1332
static emailLink( $userId, $userText)
Definition Linker.php:1410
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1883
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:1746
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:1232
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:2012
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:2142
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:1982
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:1928
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1296
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition Linker.php:1355
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:1650
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:2088
static formatRevisionSize( $size)
Definition Linker.php:1635
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:1170
static revUserLink(RevisionRecord $revRecord, $isPublic=false)
Generate a user link if the current user is allowed to view it.
Definition Linker.php:1436
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1464
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[])
Generate a rollback link for a given revision.
Definition Linker.php:1689
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:1484
static buildRollbackLink(RevisionRecord $revRecord, IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1812
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:158
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:155
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1275
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1201
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:79
Class to parse and build external user names.
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)