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