MediaWiki 1.44.0
Linker.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Linker;
24
47use Wikimedia\Assert\Assert;
50use Wikimedia\RemexHtml\Serializer\SerializerNode;
51
61class Linker {
65 public const TOOL_LINKS_NOBLOCK = 1;
66 public const TOOL_LINKS_EMAIL = 2;
67
109 public static function link(
110 $target, $html = null, $customAttribs = [], $query = [], $options = []
111 ) {
112 if ( !$target instanceof LinkTarget ) {
113 wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
114 return "<!-- ERROR -->$html";
115 }
116
117 $services = MediaWikiServices::getInstance();
118 $options = (array)$options;
119 if ( $options ) {
120 // Custom options, create new LinkRenderer
121 $linkRenderer = $services->getLinkRendererFactory()
122 ->createFromLegacyOptions( $options );
123 } else {
124 $linkRenderer = $services->getLinkRenderer();
125 }
126
127 if ( $html !== null ) {
128 $text = new HtmlArmor( $html );
129 } else {
130 $text = null;
131 }
132
133 if ( in_array( 'known', $options, true ) ) {
134 return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
135 }
136
137 if ( in_array( 'broken', $options, true ) ) {
138 return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
139 }
140
141 if ( in_array( 'noclasses', $options, true ) ) {
142 return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query );
143 }
144
145 return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
146 }
147
167 public static function linkKnown(
168 $target, $html = null, $customAttribs = [],
169 $query = [], $options = [ 'known' ]
170 ) {
171 return self::link( $target, $html, $customAttribs, $query, $options );
172 }
173
191 public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '', $hash = '' ) {
192 $nt = Title::newFromLinkTarget( $nt );
193 $attrs = [];
194 if ( $hash ) {
195 $attrs['class'] = 'mw-selflink-fragment';
196 $attrs['href'] = '#' . $hash;
197 } else {
198 // For backwards compatibility with gadgets we add selflink as well.
199 $attrs['class'] = 'mw-selflink selflink';
200 }
201 $ret = Html::rawElement( 'a', $attrs, $prefix . $html ) . $trail;
202 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
203 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
204 return $ret;
205 }
206
207 if ( $html == '' ) {
208 $html = htmlspecialchars( $nt->getPrefixedText() );
209 }
210 [ $inside, $trail ] = self::splitTrail( $trail );
211 return Html::rawElement( 'a', $attrs, $prefix . $html . $inside ) . $trail;
212 }
213
224 public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
225 // First we check whether the namespace exists or not.
226 if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
227 if ( $namespace == NS_MAIN ) {
228 $name = $context->msg( 'blanknamespace' )->text();
229 } else {
230 $name = MediaWikiServices::getInstance()->getContentLanguage()->
231 getFormattedNsText( $namespace );
232 }
233 return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
234 }
235
236 return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
237 }
238
247 private static function fnamePart( $url ) {
248 $basename = strrchr( $url, '/' );
249 if ( $basename === false ) {
250 $basename = $url;
251 } else {
252 $basename = substr( $basename, 1 );
253 }
254 return $basename;
255 }
256
267 public static function makeExternalImage( $url, $alt = '' ) {
268 if ( $alt == '' ) {
269 $alt = self::fnamePart( $url );
270 }
271 $img = '';
272 $success = ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
273 ->onLinkerMakeExternalImage( $url, $alt, $img );
274 if ( !$success ) {
275 wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
276 . "with url {$url} and alt text {$alt} to {$img}" );
277 return $img;
278 }
279 return Html::element( 'img',
280 [
281 'src' => $url,
282 'alt' => $alt
283 ]
284 );
285 }
286
325 public static function makeImageLink( Parser $parser, LinkTarget $title,
326 $file, $frameParams = [], $handlerParams = [], $time = false,
327 $query = '', $widthOption = null
328 ) {
329 $title = Title::newFromLinkTarget( $title );
330 $res = null;
331 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
332 if ( !$hookRunner->onImageBeforeProduceHTML( null, $title,
333 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
334 $file, $frameParams, $handlerParams, $time, $res,
335 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
336 $parser, $query, $widthOption )
337 ) {
338 return $res;
339 }
340
341 if ( $file && !$file->allowInlineDisplay() ) {
342 wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ' does not allow inline display' );
343 return self::link( $title );
344 }
345
346 // Clean up parameters
347 $page = $handlerParams['page'] ?? false;
348 if ( !isset( $frameParams['align'] ) ) {
349 $frameParams['align'] = '';
350 }
351 if ( !isset( $frameParams['title'] ) ) {
352 $frameParams['title'] = '';
353 }
354 if ( !isset( $frameParams['class'] ) ) {
355 $frameParams['class'] = '';
356 }
357
358 $services = MediaWikiServices::getInstance();
359 $config = $services->getMainConfig();
360 $enableLegacyMediaDOM = $config->get( MainConfigNames::ParserEnableLegacyMediaDOM );
361
362 $classes = [];
363 if (
364 !isset( $handlerParams['width'] ) &&
365 !isset( $frameParams['manualthumb'] ) &&
366 !isset( $frameParams['framed'] )
367 ) {
368 $classes[] = 'mw-default-size';
369 }
370
371 $prefix = $postfix = '';
372
373 if ( $enableLegacyMediaDOM ) {
374 if ( $frameParams['align'] == 'center' ) {
375 $prefix = '<div class="center">';
376 $postfix = '</div>';
377 $frameParams['align'] = 'none';
378 }
379 }
380
381 if ( $file && !isset( $handlerParams['width'] ) ) {
382 if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
383 // If its a vector image, and user only specifies height
384 // we don't want it to be limited by its "normal" width.
385 $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
386 $handlerParams['width'] = $svgMaxSize;
387 } else {
388 $handlerParams['width'] = $file->getWidth( $page );
389 }
390
391 if ( isset( $frameParams['thumbnail'] )
392 || isset( $frameParams['manualthumb'] )
393 || isset( $frameParams['framed'] )
394 || isset( $frameParams['frameless'] )
395 || !$handlerParams['width']
396 ) {
397 $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
398 $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
399 if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
400 $userOptionsLookup = $services->getUserOptionsLookup();
401 $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
402 }
403
404 // Reduce width for upright images when parameter 'upright' is used
405 if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
406 $frameParams['upright'] = $thumbUpright;
407 }
408
409 // For caching health: If width scaled down due to upright
410 // parameter, round to full __0 pixel to avoid the creation of a
411 // lot of odd thumbs.
412 $prefWidth = isset( $frameParams['upright'] ) ?
413 round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
414 $thumbLimits[$widthOption];
415
416 // Use width which is smaller: real image width or user preference width
417 // Unless image is scalable vector.
418 if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
419 $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
420 $handlerParams['width'] = $prefWidth;
421 }
422 }
423 }
424
425 // Parser::makeImage has a similarly named variable
426 $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
427 isset( $frameParams['manualthumb'] ) ||
428 isset( $frameParams['framed'] );
429
430 if ( $hasVisibleCaption ) {
431 if ( $enableLegacyMediaDOM ) {
432 // This is no longer needed in our new media output, since the
433 // default styling in content.media-common.less takes care of it;
434 // see T269704.
435
436 # Create a thumbnail. Alignment depends on the writing direction of
437 # the page content language (right-aligned for LTR languages,
438 # left-aligned for RTL languages)
439 # If a thumbnail width has not been provided, it is set
440 # to the default user option as specified in Language*.php
441 if ( $frameParams['align'] == '' ) {
442 $frameParams['align'] = $parser->getTargetLanguage()->alignEnd();
443 }
444 }
445 return $prefix . self::makeThumbLink2(
446 $title, $file, $frameParams, $handlerParams, $time, $query,
447 $classes, $parser
448 ) . $postfix;
449 }
450
451 $rdfaType = 'mw:File';
452
453 if ( isset( $frameParams['frameless'] ) ) {
454 $rdfaType .= '/Frameless';
455 if ( $file ) {
456 $srcWidth = $file->getWidth( $page );
457 # For "frameless" option: do not present an image bigger than the
458 # source (for bitmap-style images). This is the same behavior as the
459 # "thumb" option does it already.
460 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
461 $handlerParams['width'] = $srcWidth;
462 }
463 }
464 }
465
466 if ( $file && isset( $handlerParams['width'] ) ) {
467 # Create a resized image, without the additional thumbnail features
468 $thumb = $file->transform( $handlerParams );
469 } else {
470 $thumb = false;
471 }
472
473 $isBadFile = $file && $thumb &&
474 $parser->getBadFileLookup()->isBadFile( $title->getDBkey(), $parser->getTitle() );
475
476 if ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
477 $rdfaType = 'mw:Error ' . $rdfaType;
478 $currentExists = $file && $file->exists();
479 if ( $enableLegacyMediaDOM ) {
480 $label = $frameParams['title'];
481 } else {
482 if ( $currentExists && !$thumb ) {
483 $label = wfMessage( 'thumbnail_error', '' )->text();
484 } elseif ( $thumb && $thumb->isError() ) {
485 Assert::invariant(
486 $thumb instanceof MediaTransformError,
487 'Unknown MediaTransformOutput: ' . get_class( $thumb )
488 );
489 $label = $thumb->toText();
490 } else {
491 $label = $frameParams['alt'] ?? '';
492 }
493 }
495 $title, $label, '', '', '', (bool)$time, $handlerParams, $currentExists
496 );
497 } else {
498 self::processResponsiveImages( $file, $thumb, $handlerParams );
499 $params = [];
500 // An empty alt indicates an image is not a key part of the content
501 // and that non-visual browsers may omit it from rendering. Only
502 // set the parameter if it's explicitly requested.
503 if ( isset( $frameParams['alt'] ) ) {
504 $params['alt'] = $frameParams['alt'];
505 }
506 $params['title'] = $frameParams['title'];
507 if ( $enableLegacyMediaDOM ) {
508 $params += [
509 'valign' => $frameParams['valign'] ?? false,
510 'img-class' => $frameParams['class'],
511 ];
512 if ( isset( $frameParams['border'] ) ) {
513 $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
514 }
515 } else {
516 $params += [
517 'img-class' => 'mw-file-element',
518 ];
519 }
520 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
521 $s = $thumb->toHtml( $params );
522 }
523
524 if ( $enableLegacyMediaDOM ) {
525 if ( $frameParams['align'] != '' ) {
526 $s = Html::rawElement(
527 'div',
528 [ 'class' => 'float' . $frameParams['align'] ],
529 $s
530 );
531 }
532 return str_replace( "\n", ' ', $prefix . $s . $postfix );
533 }
534
535 $wrapper = 'span';
536 $caption = '';
537
538 if ( $frameParams['align'] != '' ) {
539 $wrapper = 'figure';
540 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
541 $classes[] = "mw-halign-{$frameParams['align']}";
542 $caption = Html::rawElement(
543 'figcaption', [], $frameParams['caption'] ?? ''
544 );
545 } elseif ( isset( $frameParams['valign'] ) ) {
546 // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
547 // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
548 // mw-valign-text-bottom
549 $classes[] = "mw-valign-{$frameParams['valign']}";
550 }
551
552 if ( isset( $frameParams['border'] ) ) {
553 $classes[] = 'mw-image-border';
554 }
555
556 if ( isset( $frameParams['class'] ) ) {
557 $classes[] = $frameParams['class'];
558 }
559
560 $attribs = [
561 'class' => $classes,
562 'typeof' => $rdfaType,
563 ];
564
565 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
566
567 return str_replace( "\n", ' ', $s );
568 }
569
578 public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
579 $mtoParams = [];
580 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
581 $mtoParams['custom-url-link'] = $frameParams['link-url'];
582 if ( isset( $frameParams['link-target'] ) ) {
583 $mtoParams['custom-target-link'] = $frameParams['link-target'];
584 }
585 if ( $parser ) {
586 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
587 foreach ( $extLinkAttrs as $name => $val ) {
588 // Currently could include 'rel' and 'target'
589 $mtoParams['parser-extlink-' . $name] = $val;
590 }
591 }
592 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
593 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
594 $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
595 $linkRenderer->normalizeTarget( $frameParams['link-title'] )
596 );
597 if ( isset( $frameParams['link-title-query'] ) ) {
598 $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
599 }
600 } elseif ( !empty( $frameParams['no-link'] ) ) {
601 // No link
602 } else {
603 $mtoParams['desc-link'] = true;
604 $mtoParams['desc-query'] = $query;
605 }
606 return $mtoParams;
607 }
608
621 public static function makeThumbLinkObj(
622 LinkTarget $title, $file, $label = '', $alt = '', $align = null,
623 $params = [], $framed = false, $manualthumb = ''
624 ) {
625 $frameParams = [
626 'alt' => $alt,
627 'caption' => $label,
628 'align' => $align
629 ];
630 $classes = [];
631 if ( $manualthumb ) {
632 $frameParams['manualthumb'] = $manualthumb;
633 } elseif ( $framed ) {
634 $frameParams['framed'] = true;
635 } elseif ( !isset( $params['width'] ) ) {
636 $classes[] = 'mw-default-size';
637 }
639 $title, $file, $frameParams, $params, false, '', $classes
640 );
641 }
642
654 public static function makeThumbLink2(
655 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
656 $time = false, $query = '', array $classes = [], ?Parser $parser = null
657 ) {
658 $exists = $file && $file->exists();
659
660 $services = MediaWikiServices::getInstance();
661 $enableLegacyMediaDOM = $services->getMainConfig()->get( MainConfigNames::ParserEnableLegacyMediaDOM );
662
663 $page = $handlerParams['page'] ?? false;
664 $lang = $handlerParams['lang'] ?? false;
665
666 if ( !isset( $frameParams['align'] ) ) {
667 $frameParams['align'] = '';
668 if ( $enableLegacyMediaDOM ) {
669 $frameParams['align'] = 'right';
670 }
671 }
672 if ( !isset( $frameParams['caption'] ) ) {
673 $frameParams['caption'] = '';
674 }
675
676 if ( empty( $handlerParams['width'] ) ) {
677 // Reduce width for upright images when parameter 'upright' is used
678 $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
679 }
680
681 $thumb = false;
682 $noscale = false;
683 $manualthumb = false;
684 $manual_title = '';
685 $rdfaType = 'mw:File/Thumb';
686
687 if ( !$exists ) {
688 // Same precedence as the $exists case
689 if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
690 $rdfaType = 'mw:File/Frame';
691 }
692 $outerWidth = $handlerParams['width'] + 2;
693 } else {
694 if ( isset( $frameParams['manualthumb'] ) ) {
695 # Use manually specified thumbnail
696 $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
697 if ( $manual_title ) {
698 $manual_img = $services->getRepoGroup()
699 ->findFile( $manual_title );
700 if ( $manual_img ) {
701 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
702 $manualthumb = true;
703 }
704 }
705 } else {
706 $srcWidth = $file->getWidth( $page );
707 if ( isset( $frameParams['framed'] ) ) {
708 $rdfaType = 'mw:File/Frame';
709 if ( !$file->isVectorized() ) {
710 // Use image dimensions, don't scale
711 $noscale = true;
712 } else {
713 // framed is unscaled, but for vectorized images
714 // we need to a width for scaling up for the high density variants
715 $handlerParams['width'] = $srcWidth;
716 }
717 }
718
719 // Do not present an image bigger than the source, for bitmap-style images
720 // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
721 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
722 $handlerParams['width'] = $srcWidth;
723 }
724
725 $thumb = $noscale
726 ? $file->getUnscaledThumb( $handlerParams )
727 : $file->transform( $handlerParams );
728 }
729
730 if ( $thumb ) {
731 $outerWidth = $thumb->getWidth() + 2;
732 } else {
733 $outerWidth = $handlerParams['width'] + 2;
734 }
735 }
736
737 if ( !$enableLegacyMediaDOM && $parser && $rdfaType === 'mw:File/Thumb' ) {
738 $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
739 }
740
741 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
742 $linkTitleQuery = [];
743 if ( $page || $lang ) {
744 if ( $page ) {
745 $linkTitleQuery['page'] = $page;
746 }
747 if ( $lang ) {
748 $linkTitleQuery['lang'] = $lang;
749 }
750 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
751 # So we don't need to pass it here in $query. However, the URL for the
752 # zoom icon still needs it, so we make a unique query for it. See T16771
753 $url = wfAppendQuery( $url, $linkTitleQuery );
754 }
755
756 if ( $manualthumb
757 && !isset( $frameParams['link-title'] )
758 && !isset( $frameParams['link-url'] )
759 && !isset( $frameParams['no-link'] ) ) {
760 $frameParams['link-title'] = $title;
761 $frameParams['link-title-query'] = $linkTitleQuery;
762 }
763
764 if ( $frameParams['align'] != '' ) {
765 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
766 $classes[] = "mw-halign-{$frameParams['align']}";
767 }
768
769 if ( isset( $frameParams['class'] ) ) {
770 $classes[] = $frameParams['class'];
771 }
772
773 $s = '';
774
775 if ( $enableLegacyMediaDOM ) {
776 $s .= "<div class=\"thumb t{$frameParams['align']}\">"
777 . "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
778 }
779
780 $isBadFile = $exists && $thumb && $parser &&
781 $parser->getBadFileLookup()->isBadFile(
782 $manualthumb ? $manual_title : $title->getDBkey(),
783 $parser->getTitle()
784 );
785
786 if ( !$exists ) {
787 $rdfaType = 'mw:Error ' . $rdfaType;
788 $label = '';
789 if ( !$enableLegacyMediaDOM ) {
790 $label = $frameParams['alt'] ?? '';
791 }
793 $title, $label, '', '', '', (bool)$time, $handlerParams, false
794 );
795 $zoomIcon = '';
796 } elseif ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
797 $rdfaType = 'mw:Error ' . $rdfaType;
798 if ( $enableLegacyMediaDOM ) {
799 if ( !$thumb ) {
800 $s .= wfMessage( 'thumbnail_error', '' )->escaped();
801 } else {
803 $title, '', '', '', '', (bool)$time, $handlerParams, true
804 );
805 }
806 } else {
807 if ( $thumb && $thumb->isError() ) {
808 Assert::invariant(
809 $thumb instanceof MediaTransformError,
810 'Unknown MediaTransformOutput: ' . get_class( $thumb )
811 );
812 $label = $thumb->toText();
813 } elseif ( !$thumb ) {
814 $label = wfMessage( 'thumbnail_error', '' )->text();
815 } else {
816 $label = '';
817 }
819 $title, $label, '', '', '', (bool)$time, $handlerParams, true
820 );
821 }
822 $zoomIcon = '';
823 } else {
824 if ( !$noscale && !$manualthumb ) {
825 self::processResponsiveImages( $file, $thumb, $handlerParams );
826 }
827 $params = [];
828 // An empty alt indicates an image is not a key part of the content
829 // and that non-visual browsers may omit it from rendering. Only
830 // set the parameter if it's explicitly requested.
831 if ( isset( $frameParams['alt'] ) ) {
832 $params['alt'] = $frameParams['alt'];
833 }
834 if ( $enableLegacyMediaDOM ) {
835 $params += [
836 'img-class' => ( isset( $frameParams['class'] ) && $frameParams['class'] !== ''
837 ? $frameParams['class'] . ' '
838 : '' ) . 'thumbimage'
839 ];
840 } else {
841 $params += [
842 'img-class' => 'mw-file-element',
843 ];
844 // Only thumbs gets the magnify link
845 if ( $rdfaType === 'mw:File/Thumb' ) {
846 $params['magnify-resource'] = $url;
847 }
848 }
849 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
850 $s .= $thumb->toHtml( $params );
851 if ( isset( $frameParams['framed'] ) ) {
852 $zoomIcon = '';
853 } else {
854 $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
855 Html::rawElement( 'a', [
856 'href' => $url,
857 'class' => 'internal',
858 'title' => wfMessage( 'thumbnail-more' )->text(),
859 ] )
860 );
861 }
862 }
863
864 if ( $enableLegacyMediaDOM ) {
865 $s .= ' <div class="thumbcaption">' . $zoomIcon . $frameParams['caption'] . '</div></div></div>';
866 return str_replace( "\n", ' ', $s );
867 }
868
869 $s .= Html::rawElement(
870 'figcaption', [], $frameParams['caption'] ?? ''
871 );
872
873 $attribs = [
874 'class' => $classes,
875 'typeof' => $rdfaType,
876 ];
877
878 $s = Html::rawElement( 'figure', $attribs, $s );
879
880 return str_replace( "\n", ' ', $s );
881 }
882
891 public static function processResponsiveImages( $file, $thumb, $hp ) {
892 $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
893 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
894 $hp15 = $hp;
895 $hp15['width'] = round( $hp['width'] * 1.5 );
896 $hp20 = $hp;
897 $hp20['width'] = $hp['width'] * 2;
898 if ( isset( $hp['height'] ) ) {
899 $hp15['height'] = round( $hp['height'] * 1.5 );
900 $hp20['height'] = $hp['height'] * 2;
901 }
902
903 $thumb15 = $file->transform( $hp15 );
904 $thumb20 = $file->transform( $hp20 );
905 if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
906 $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
907 }
908 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
909 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
910 }
911 }
912 }
913
928 public static function makeBrokenImageLinkObj(
929 $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
930 $time = false, array $handlerParams = [], bool $currentExists = false
931 ) {
932 if ( !$title instanceof LinkTarget ) {
933 wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
934 return "<!-- ERROR -->" . htmlspecialchars( $label );
935 }
936
937 $title = Title::newFromLinkTarget( $title );
938 $services = MediaWikiServices::getInstance();
939 $mainConfig = $services->getMainConfig();
940 $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
941 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
942 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
943 if ( $label == '' ) {
944 $label = $title->getPrefixedText();
945 }
946
947 $html = Html::element( 'span', [
948 'class' => 'mw-file-element mw-broken-media',
949 // These data attributes are used to dynamically size the span, see T273013
950 'data-width' => $handlerParams['width'] ?? null,
951 'data-height' => $handlerParams['height'] ?? null,
952 ], $label );
953
954 if ( $mainConfig->get( MainConfigNames::ParserEnableLegacyMediaDOM ) ) {
955 $html = htmlspecialchars( $label, ENT_COMPAT );
956 }
957
958 $repoGroup = $services->getRepoGroup();
959 $currentExists = $currentExists ||
960 ( $time && $repoGroup->findFile( $title ) !== false );
961
962 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
963 && !$currentExists
964 ) {
965 if (
966 $title->inNamespace( NS_FILE ) &&
967 $repoGroup->getLocalRepo()->checkRedirect( $title )
968 ) {
969 // We already know it's a redirect, so mark it accordingly
970 return self::link(
971 $title,
972 $html,
973 [ 'class' => 'mw-redirect' ],
974 wfCgiToArray( $query ),
975 [ 'known', 'noclasses' ]
976 );
977 }
978 return Html::rawElement( 'a', [
979 'href' => self::getUploadUrl( $title, $query ),
980 'class' => 'new',
981 'title' => $title->getPrefixedText()
982 ], $html );
983 }
984 return self::link(
985 $title,
986 $html,
987 [],
988 wfCgiToArray( $query ),
989 [ 'known', 'noclasses' ]
990 );
991 }
992
1001 public static function getUploadUrl( $destFile, $query = '' ) {
1002 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1003 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
1004 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
1005 $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
1006 if ( $query != '' ) {
1007 $q .= '&' . $query;
1008 }
1009
1010 if ( $uploadMissingFileUrl ) {
1011 return wfAppendQuery( $uploadMissingFileUrl, $q );
1012 }
1013
1014 if ( $uploadNavigationUrl ) {
1015 return wfAppendQuery( $uploadNavigationUrl, $q );
1016 }
1017
1018 $upload = SpecialPage::getTitleFor( 'Upload' );
1019
1020 return $upload->getLocalURL( $q );
1021 }
1022
1032 public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
1033 $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
1034 $title, [ 'time' => $time ]
1035 );
1036 return self::makeMediaLinkFile( $title, $img, $html );
1037 }
1038
1051 public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
1052 if ( $file && $file->exists() ) {
1053 $url = $file->getUrl();
1054 $class = 'internal';
1055 } else {
1056 $url = self::getUploadUrl( $title );
1057 $class = 'new';
1058 }
1059
1060 $alt = $title->getText();
1061 if ( $html == '' ) {
1062 $html = $alt;
1063 }
1064
1065 $ret = '';
1066 $attribs = [
1067 'href' => $url,
1068 'class' => $class,
1069 'title' => $alt
1070 ];
1071
1072 if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
1073 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
1074 ) {
1075 wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
1076 . "with url {$url} and text {$html} to {$ret}" );
1077 return $ret;
1078 }
1079
1080 return Html::rawElement( 'a', $attribs, $html );
1081 }
1082
1093 public static function specialLink( $name, $key = '' ) {
1094 $queryPos = strpos( $name, '?' );
1095 if ( $queryPos !== false ) {
1096 $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
1097 $name = substr( $name, 0, $queryPos );
1098 } else {
1099 $getParams = [];
1100 }
1101
1102 $slashPos = strpos( $name, '/' );
1103 if ( $slashPos !== false ) {
1104 $subpage = substr( $name, $slashPos + 1 );
1105 $name = substr( $name, 0, $slashPos );
1106 } else {
1107 $subpage = false;
1108 }
1109
1110 if ( $key == '' ) {
1111 $key = strtolower( $name );
1112 }
1113
1114 return self::linkKnown(
1115 SpecialPage::getTitleFor( $name, $subpage ),
1116 wfMessage( $key )->escaped(),
1117 [],
1118 $getParams
1119 );
1120 }
1121
1142 public static function makeExternalLink( $url, $text, $escape = true,
1143 $linktype = '', $attribs = [], $title = null
1144 ) {
1145 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1146 global $wgTitle;
1147 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1148 return $linkRenderer->makeExternalLink(
1149 $url,
1150 $escape ? $text : new HtmlArmor( $text ),
1151 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1152 $linktype,
1153 $attribs
1154 );
1155 }
1156
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 return MediaWikiServices::getInstance()->getUserLinkRenderer()
1184 ->userLink(
1185 new UserIdentityValue( $userId, (string)$userName ),
1186 RequestContext::getMain(),
1187 $altUserName === false ? null : (string)$altUserName,
1188 $attributes
1189 );
1190 }
1191
1210 public static function userToolLinkArray(
1211 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1212 ): array {
1213 $services = MediaWikiServices::getInstance();
1214 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1215 $talkable = !( $disableAnonTalk && $userId == 0 );
1216 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1217 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1218
1219 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1220 // No tools for an external user
1221 return [];
1222 }
1223
1224 $items = [];
1225 if ( $talkable ) {
1226 $items[] = self::userTalkLink( $userId, $userText );
1227 }
1228 if ( $userId ) {
1229 // check if the user has an edit
1230 $attribs = [];
1231 $attribs['class'] = 'mw-usertoollinks-contribs';
1232 if ( $redContribsWhenNoEdits ) {
1233 if ( $edits === null ) {
1234 $user = UserIdentityValue::newRegistered( $userId, $userText );
1235 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1236 }
1237 if ( $edits === 0 ) {
1238 // Note: "new" class is inappropriate here, as "new" class
1239 // should only be used for pages that do not exist.
1240 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1241 }
1242 }
1243 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1244
1245 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1246 }
1247 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1248 if ( $blockable && $userCanBlock ) {
1249 $items[] = self::blockLink( $userId, $userText );
1250 }
1251
1252 if (
1253 $addEmailLink
1254 && MediaWikiServices::getInstance()->getEmailUserFactory()
1255 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1256 ->canSend()
1257 ->isGood()
1258 ) {
1259 $items[] = self::emailLink( $userId, $userText );
1260 }
1261
1262 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1263
1264 return $items;
1265 }
1266
1274 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1275 global $wgLang;
1276
1277 if ( !$items ) {
1278 return '';
1279 }
1280
1281 if ( $useParentheses ) {
1282 return wfMessage( 'word-separator' )->escaped()
1283 . '<span class="mw-usertoollinks">'
1284 . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1285 . '</span>';
1286 }
1287
1288 $tools = [];
1289 foreach ( $items as $tool ) {
1290 $tools[] = Html::rawElement( 'span', [], $tool );
1291 }
1292 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1293 implode( ' ', $tools ) . '</span>';
1294 }
1295
1310 public static function userToolLinks(
1311 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1312 $useParentheses = true
1313 ) {
1314 if ( $userText === '' ) {
1315 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1316 'that need to be fixed?' );
1317 return ' ' . wfMessage( 'empty-username' )->parse();
1318 }
1319
1320 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1321 return self::renderUserToolLinksArray( $items, $useParentheses );
1322 }
1323
1333 public static function userToolLinksRedContribs(
1334 $userId, $userText, $edits = null, $useParentheses = true
1335 ) {
1336 return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1337 }
1338
1345 public static function userTalkLink( $userId, $userText ) {
1346 if ( $userText === '' ) {
1347 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1348 'that need to be fixed?' );
1349 return wfMessage( 'empty-username' )->parse();
1350 }
1351
1352 $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1353 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1354 $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1355
1356 return $userTalkPage
1357 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1358 : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1359 }
1360
1367 public static function blockLink( $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 $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1375 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1376
1377 return self::link( $blockPage,
1378 wfMessage( 'blocklink' )->escaped(),
1379 $moreLinkAttribs
1380 );
1381 }
1382
1388 public static function emailLink( $userId, $userText ) {
1389 if ( $userText === '' ) {
1390 wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1391 'that need to be fixed?' );
1392 return wfMessage( 'empty-username' )->parse();
1393 }
1394
1395 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1396 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1397 return self::link( $emailPage,
1398 wfMessage( 'emaillink' )->escaped(),
1399 $moreLinkAttribs
1400 );
1401 }
1402
1414 public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1415 // TODO inject authority
1416 $authority = RequestContext::getMain()->getAuthority();
1417
1418 $revUser = $revRecord->getUser(
1419 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1420 $authority
1421 );
1422 if ( $revUser ) {
1423 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1424 } else {
1425 // User is deleted and we can't (or don't want to) view it
1426 $link = wfMessage( 'rev-deleted-user' )->escaped();
1427 }
1428
1429 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1430 $class = self::getRevisionDeletedClass( $revRecord );
1431 return '<span class="' . $class . '">' . $link . '</span>';
1432 }
1433 return $link;
1434 }
1435
1442 public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1443 $class = 'history-deleted';
1444 if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1445 $class .= ' mw-history-suppressed';
1446 }
1447 return $class;
1448 }
1449
1462 public static function revUserTools(
1463 RevisionRecord $revRecord,
1464 $isPublic = false,
1465 $useParentheses = true
1466 ) {
1467 // TODO inject authority
1468 $authority = RequestContext::getMain()->getAuthority();
1469
1470 $revUser = $revRecord->getUser(
1471 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1472 $authority
1473 );
1474 if ( $revUser ) {
1475 $link = self::userLink(
1476 $revUser->getId(),
1477 $revUser->getName(),
1478 false,
1479 [ 'data-mw-revid' => $revRecord->getId() ]
1480 ) . self::userToolLinks(
1481 $revUser->getId(),
1482 $revUser->getName(),
1483 false,
1484 0,
1485 null,
1486 $useParentheses
1487 );
1488 } else {
1489 // User is deleted and we can't (or don't want to) view it
1490 $link = wfMessage( 'rev-deleted-user' )->escaped();
1491 }
1492
1493 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1494 $class = self::getRevisionDeletedClass( $revRecord );
1495 return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1496 }
1497 return $link;
1498 }
1499
1510 public static function expandLocalLinks( string $html ) {
1511 return HtmlHelper::modifyElements(
1512 $html,
1513 static function ( SerializerNode $node ): bool {
1514 return $node->name === 'a' && isset( $node->attrs['href'] );
1515 },
1516 static function ( SerializerNode $node ): SerializerNode {
1517 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1518 $node->attrs['href'] =
1519 $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE ) ?? false;
1520 return $node;
1521 }
1522 );
1523 }
1524
1531 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1532 # Valid link forms:
1533 # Foobar -- normal
1534 # :Foobar -- override special treatment of prefix (images, language links)
1535 # /Foobar -- convert to CurrentPage/Foobar
1536 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1537 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1538 # ../Foobar -- convert to CurrentPage/Foobar,
1539 # (from CurrentPage/CurrentSubPage)
1540 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1541 # (from CurrentPage/CurrentSubPage)
1542
1543 $ret = $target; # default return value is no change
1544
1545 # Some namespaces don't allow subpages,
1546 # so only perform processing if subpages are allowed
1547 if (
1548 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1549 hasSubpages( $contextTitle->getNamespace() )
1550 ) {
1551 $hash = strpos( $target, '#' );
1552 if ( $hash !== false ) {
1553 $suffix = substr( $target, $hash );
1554 $target = substr( $target, 0, $hash );
1555 } else {
1556 $suffix = '';
1557 }
1558 # T9425
1559 $target = trim( $target );
1560 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1561 getPrefixedText( $contextTitle );
1562 # Look at the first character
1563 if ( $target != '' && $target[0] === '/' ) {
1564 # / at end means we don't want the slash to be shown
1565 $m = [];
1566 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1567 if ( $trailingSlashes ) {
1568 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1569 } else {
1570 $noslash = substr( $target, 1 );
1571 }
1572
1573 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1574 if ( $text === '' ) {
1575 $text = $target . $suffix;
1576 } # this might be changed for ugliness reasons
1577 } else {
1578 # check for .. subpage backlinks
1579 $dotdotcount = 0;
1580 $nodotdot = $target;
1581 while ( str_starts_with( $nodotdot, '../' ) ) {
1582 ++$dotdotcount;
1583 $nodotdot = substr( $nodotdot, 3 );
1584 }
1585 if ( $dotdotcount > 0 ) {
1586 $exploded = explode( '/', $contextPrefixedText );
1587 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1588 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1589 # / at the end means don't show full path
1590 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1591 $nodotdot = rtrim( $nodotdot, '/' );
1592 if ( $text === '' ) {
1593 $text = $nodotdot . $suffix;
1594 }
1595 }
1596 $nodotdot = trim( $nodotdot );
1597 if ( $nodotdot != '' ) {
1598 $ret .= '/' . $nodotdot;
1599 }
1600 $ret .= $suffix;
1601 }
1602 }
1603 }
1604 }
1605
1606 return $ret;
1607 }
1608
1614 public static function formatRevisionSize( $size ) {
1615 if ( $size == 0 ) {
1616 $stxt = wfMessage( 'historyempty' )->escaped();
1617 } else {
1618 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1619 }
1620 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1621 }
1622
1629 public static function splitTrail( $trail ) {
1630 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1631 $inside = '';
1632 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1633 [ , $inside, $trail ] = $m;
1634 }
1635 return [ $inside, $trail ];
1636 }
1637
1668 public static function generateRollback(
1669 RevisionRecord $revRecord,
1670 ?IContextSource $context = null,
1671 $options = []
1672 ) {
1673 $context ??= RequestContext::getMain();
1674
1675 $editCount = self::getRollbackEditCount( $revRecord );
1676 if ( $editCount === false ) {
1677 return '';
1678 }
1679
1680 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1681
1682 $services = MediaWikiServices::getInstance();
1683 // Allow extensions to modify the rollback link.
1684 // Abort further execution if the extension wants full control over the link.
1685 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1686 $revRecord, $context, $options, $inner ) ) {
1687 return $inner;
1688 }
1689
1690 if ( !in_array( 'noBrackets', $options, true ) ) {
1691 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1692 }
1693
1694 if ( $services->getUserOptionsLookup()
1695 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1696 ) {
1697 $services->getStatsFactory()
1698 ->getCounter( 'rollbackconfirmation_event_load_total' )
1699 ->copyToStatsdAt( 'rollbackconfirmation.event.load' )
1700 ->increment();
1701 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1702 }
1703
1704 return '<span class="mw-rollback-link">' . $inner . '</span>';
1705 }
1706
1725 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1726 if ( func_num_args() > 1 ) {
1727 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1728 }
1729 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1730 ->get( MainConfigNames::ShowRollbackEditCount );
1731
1732 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1733 // Nothing has happened, indicate this by returning 'null'
1734 return null;
1735 }
1736
1737 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1738
1739 // Up to the value of $wgShowRollbackEditCount revisions are counted
1740 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1741 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1742 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1743 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1744 ->limit( $showRollbackEditCount + 1 )
1745 ->caller( __METHOD__ )->fetchResultSet();
1746
1747 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1748 $revUserText = $revUser ? $revUser->getName() : '';
1749
1750 $editCount = 0;
1751 $moreRevs = false;
1752 foreach ( $res as $row ) {
1753 if ( $row->rev_user_text != $revUserText ) {
1754 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1755 || $row->rev_deleted & RevisionRecord::DELETED_USER
1756 ) {
1757 // If the user or the text of the revision we might rollback
1758 // to is deleted in some way we can't rollback. Similar to
1759 // the checks in WikiPage::commitRollback.
1760 return false;
1761 }
1762 $moreRevs = true;
1763 break;
1764 }
1765 $editCount++;
1766 }
1767
1768 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1769 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1770 // and there weren't any other revisions. That means that the current user is the only
1771 // editor, so we can't rollback
1772 return false;
1773 }
1774 return $editCount;
1775 }
1776
1791 public static function buildRollbackLink(
1792 RevisionRecord $revRecord,
1793 ?IContextSource $context = null,
1794 $editCount = false
1795 ) {
1796 $config = MediaWikiServices::getInstance()->getMainConfig();
1797 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1798 $miserMode = $config->get( MainConfigNames::MiserMode );
1799 // To config which pages are affected by miser mode
1800 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1801
1802 $context ??= RequestContext::getMain();
1803
1804 $title = $revRecord->getPageAsLinkTarget();
1805 $revUser = $revRecord->getUser();
1806 $revUserText = $revUser ? $revUser->getName() : '';
1807
1808 $query = [
1809 'action' => 'rollback',
1810 'from' => $revUserText,
1811 'token' => $context->getUser()->getEditToken( 'rollback' ),
1812 ];
1813
1814 $attrs = [
1815 'data-mw' => 'interface',
1816 'title' => $context->msg( 'tooltip-rollback' )->text()
1817 ];
1818
1819 $options = [ 'known', 'noclasses' ];
1820
1821 if ( $context->getRequest()->getBool( 'bot' ) ) {
1822 // T17999
1823 $query['hidediff'] = '1';
1824 $query['bot'] = '1';
1825 }
1826
1827 if ( $miserMode ) {
1828 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1829 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1830 $showRollbackEditCount = false;
1831 break;
1832 }
1833 }
1834 }
1835
1836 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1837 $msg = [ 'rollbacklink' ];
1838 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1839 if ( !is_numeric( $editCount ) ) {
1840 $editCount = self::getRollbackEditCount( $revRecord );
1841 }
1842
1843 if ( $editCount > $showRollbackEditCount ) {
1844 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1845 } elseif ( $editCount ) {
1846 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1847 }
1848 }
1849
1850 $html = $context->msg( ...$msg )->parse();
1851 return self::link( $title, $html, $attrs, $query, $options );
1852 }
1853
1862 public static function formatHiddenCategories( $hiddencats ) {
1863 $outText = '';
1864 if ( count( $hiddencats ) > 0 ) {
1865 # Construct the HTML
1866 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1867 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1868 $outText .= "</div><ul>\n";
1869
1870 foreach ( $hiddencats as $titleObj ) {
1871 # If it's hidden, it must exist - no need to check with a LinkBatch
1872 $outText .= '<li>'
1873 . self::link( $titleObj, null, [], [], 'known' )
1874 . "</li>\n";
1875 }
1876 $outText .= '</ul>';
1877 }
1878 return $outText;
1879 }
1880
1884 private static function getContextFromMain() {
1885 $context = RequestContext::getMain();
1886 $context = new DerivativeContext( $context );
1887 return $context;
1888 }
1889
1907 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1908 if ( !$localizer ) {
1909 $localizer = self::getContextFromMain();
1910 }
1911 $message = $localizer->msg( "tooltip-$name", $msgParams );
1912 // Set a default tooltip for subject namespace tabs if that hasn't
1913 // been defined. See T22126
1914 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1915 $message = $localizer->msg( 'tooltip-ca-nstab' );
1916 }
1917
1918 if ( $message->isDisabled() ) {
1919 $tooltip = false;
1920 } else {
1921 $tooltip = $message->text();
1922 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1923 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1924 }
1925
1926 $options = (array)$options;
1927
1928 if ( in_array( 'nonexisting', $options ) ) {
1929 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1930 }
1931 if ( in_array( 'withaccess', $options ) ) {
1932 $accesskey = self::accesskey( $name, $localizer );
1933 if ( $accesskey !== false ) {
1934 // Should be build the same as in jquery.accessKeyLabel.js
1935 if ( $tooltip === false || $tooltip === '' ) {
1936 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1937 } else {
1938 $tooltip .= $localizer->msg( 'word-separator' )->text();
1939 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1940 }
1941 }
1942 }
1943
1944 return $tooltip;
1945 }
1946
1948 public static $accesskeycache;
1949
1962 public static function accesskey( $name, $localizer = null ) {
1963 if ( !isset( self::$accesskeycache[$name] ) ) {
1964 if ( !$localizer ) {
1965 $localizer = self::getContextFromMain();
1966 }
1967 $msg = $localizer->msg( "accesskey-$name" );
1968 // Set a default accesskey for subject namespace tabs if an
1969 // accesskey has not been defined. See T22126
1970 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1971 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1972 }
1973 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1974 }
1975 return self::$accesskeycache[$name];
1976 }
1977
1992 public static function getRevDeleteLink(
1993 Authority $performer,
1994 RevisionRecord $revRecord,
1995 LinkTarget $title
1996 ) {
1997 $canHide = $performer->isAllowed( 'deleterevision' );
1998 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
1999 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
2000 return '';
2001 }
2002
2003 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2004 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2005 }
2006 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2007 getPrefixedDBkey( $title );
2008 if ( $revRecord->getId() ) {
2009 // RevDelete links using revision ID are stable across
2010 // page deletion and undeletion; use when possible.
2011 $query = [
2012 'type' => 'revision',
2013 'target' => $prefixedDbKey,
2014 'ids' => $revRecord->getId()
2015 ];
2016 } else {
2017 // Older deleted entries didn't save a revision ID.
2018 // We have to refer to these by timestamp, ick!
2019 $query = [
2020 'type' => 'archive',
2021 'target' => $prefixedDbKey,
2022 'ids' => $revRecord->getTimestamp()
2023 ];
2024 }
2025 return self::revDeleteLink(
2026 $query,
2027 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2028 $canHide
2029 );
2030 }
2031
2044 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2045 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2046 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2047 $html = wfMessage( $msgKey )->escaped();
2048 $tag = $restricted ? 'strong' : 'span';
2049 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2050 return Html::rawElement(
2051 $tag,
2052 [ 'class' => 'mw-revdelundel-link' ],
2053 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2054 );
2055 }
2056
2068 public static function revDeleteLinkDisabled( $delete = true ) {
2069 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2070 $html = wfMessage( $msgKey )->escaped();
2071 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2072 return Html::rawElement( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2073 }
2074
2088 public static function tooltipAndAccesskeyAttribs(
2089 $name,
2090 array $msgParams = [],
2091 $options = null,
2092 $localizer = null
2093 ) {
2094 $options = (array)$options;
2095 $options[] = 'withaccess';
2096
2097 // Get optional parameters from global context if any missing.
2098 if ( !$localizer ) {
2099 $localizer = self::getContextFromMain();
2100 }
2101
2102 $attribs = [
2103 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2104 'accesskey' => self::accesskey( $name, $localizer )
2105 ];
2106 if ( $attribs['title'] === false ) {
2107 unset( $attribs['title'] );
2108 }
2109 if ( $attribs['accesskey'] === false ) {
2110 unset( $attribs['accesskey'] );
2111 }
2112 return $attribs;
2113 }
2114
2122 public static function tooltip( $name, $options = null ) {
2123 $tooltip = self::titleAttrib( $name, $options );
2124 if ( $tooltip === false ) {
2125 return '';
2126 }
2127 return Html::expandAttributes( [
2128 'title' => $tooltip
2129 ] );
2130 }
2131
2132}
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
const PROTO_RELATIVE
Definition Defines.php:233
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:565
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:565
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.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
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:57
Some internal bits split of from Skin.php.
Definition Linker.php:61
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition Linker.php:1510
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2044
static link( $target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition Linker.php:109
static string false[] $accesskeycache
Definition Linker.php:1948
static blockLink( $userId, $userText)
Definition Linker.php:1367
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='', $hash='')
Make appropriate markup for a link to the current article.
Definition Linker.php:191
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null, $localizer=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2088
static getUploadUrl( $destFile, $query='')
Get the URL to upload a certain file.
Definition Linker.php:1001
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:325
static makeMediaLinkObj( $title, $html='', $time=false)
Create a direct link to a given uploaded file.
Definition Linker.php:1032
static processResponsiveImages( $file, $thumb, $hp)
Process responsive images: add 1.5x and 2x subimages to the thumbnail, where applicable.
Definition Linker.php:891
static userTalkLink( $userId, $userText)
Definition Linker.php:1345
static generateRollback(RevisionRecord $revRecord, ?IContextSource $context=null, $options=[])
Generate a rollback link for a given revision.
Definition Linker.php:1668
static buildRollbackLink(RevisionRecord $revRecord, ?IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1791
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1531
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:1093
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:1310
static emailLink( $userId, $userText)
Definition Linker.php:1388
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1862
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition Linker.php:224
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:1725
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1142
static userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1210
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition Linker.php:578
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition Linker.php:167
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:1992
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query='', array $classes=[], ?Parser $parser=null)
Definition Linker.php:654
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:267
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition Linker.php:2122
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[], bool $currentExists=false)
Make a "broken" link to an image.
Definition Linker.php:928
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:1051
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1962
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:1907
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1274
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition Linker.php:1333
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:1629
const TOOL_LINKS_NOBLOCK
Flags for userToolLinks()
Definition Linker.php:65
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:2068
static formatRevisionSize( $size)
Definition Linker.php:1614
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:621
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:1414
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1442
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:1462
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:157
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:147
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1255
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1181
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,...
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.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:32
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)