MediaWiki master
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
361 $classes = [];
362 if (
363 !isset( $handlerParams['width'] ) &&
364 !isset( $frameParams['manualthumb'] ) &&
365 !isset( $frameParams['framed'] )
366 ) {
367 $classes[] = 'mw-default-size';
368 }
369
370 $prefix = $postfix = '';
371
372 if ( $file && !isset( $handlerParams['width'] ) ) {
373 if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
374 // If its a vector image, and user only specifies height
375 // we don't want it to be limited by its "normal" width.
376 $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
377 $handlerParams['width'] = $svgMaxSize;
378 } else {
379 $handlerParams['width'] = $file->getWidth( $page );
380 }
381
382 if ( isset( $frameParams['thumbnail'] )
383 || isset( $frameParams['manualthumb'] )
384 || isset( $frameParams['framed'] )
385 || isset( $frameParams['frameless'] )
386 || !$handlerParams['width']
387 ) {
388 $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
389 $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
390 if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
391 $userOptionsLookup = $services->getUserOptionsLookup();
392 $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
393 }
394
395 // Reduce width for upright images when parameter 'upright' is used
396 if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
397 $frameParams['upright'] = $thumbUpright;
398 }
399
400 // For caching health: If width scaled down due to upright
401 // parameter, round to full __0 pixel to avoid the creation of a
402 // lot of odd thumbs.
403 $prefWidth = isset( $frameParams['upright'] ) ?
404 round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
405 $thumbLimits[$widthOption];
406
407 // Use width which is smaller: real image width or user preference width
408 // Unless image is scalable vector.
409 if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
410 $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
411 $handlerParams['width'] = $prefWidth;
412 }
413 }
414 }
415
416 // Parser::makeImage has a similarly named variable
417 $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
418 isset( $frameParams['manualthumb'] ) ||
419 isset( $frameParams['framed'] );
420
421 if ( $hasVisibleCaption ) {
422 return $prefix . self::makeThumbLink2(
423 $title, $file, $frameParams, $handlerParams, $time, $query,
424 $classes, $parser
425 ) . $postfix;
426 }
427
428 $rdfaType = 'mw:File';
429
430 if ( isset( $frameParams['frameless'] ) ) {
431 $rdfaType .= '/Frameless';
432 if ( $file ) {
433 $srcWidth = $file->getWidth( $page );
434 # For "frameless" option: do not present an image bigger than the
435 # source (for bitmap-style images). This is the same behavior as the
436 # "thumb" option does it already.
437 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
438 $handlerParams['width'] = $srcWidth;
439 }
440 }
441 }
442
443 if ( $file && isset( $handlerParams['width'] ) ) {
444 # Create a resized image, without the additional thumbnail features
445 $thumb = $file->transform( $handlerParams );
446 } else {
447 $thumb = false;
448 }
449
450 $isBadFile = $file && $thumb &&
451 $parser->getBadFileLookup()->isBadFile( $title->getDBkey(), $parser->getTitle() );
452
453 if ( !$thumb || $thumb->isError() || $isBadFile ) {
454 $rdfaType = 'mw:Error ' . $rdfaType;
455 $currentExists = $file && $file->exists();
456 if ( $currentExists && !$thumb ) {
457 $label = wfMessage( 'thumbnail_error', '' )->text();
458 } elseif ( $thumb && $thumb->isError() ) {
459 Assert::invariant(
460 $thumb instanceof MediaTransformError,
461 'Unknown MediaTransformOutput: ' . get_class( $thumb )
462 );
463 $label = $thumb->toText();
464 } else {
465 $label = $frameParams['alt'] ?? '';
466 }
468 $title, $label, '', '', '', (bool)$time, $handlerParams, $currentExists
469 );
470 } else {
471 self::processResponsiveImages( $file, $thumb, $handlerParams );
472 $params = [];
473 // An empty alt indicates an image is not a key part of the content
474 // and that non-visual browsers may omit it from rendering. Only
475 // set the parameter if it's explicitly requested.
476 if ( isset( $frameParams['alt'] ) ) {
477 $params['alt'] = $frameParams['alt'];
478 }
479 $params['title'] = $frameParams['title'];
480 $params += [
481 'img-class' => 'mw-file-element',
482 ];
483 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
484 $s = $thumb->toHtml( $params );
485 }
486
487 $wrapper = 'span';
488 $caption = '';
489
490 if ( $frameParams['align'] != '' ) {
491 $wrapper = 'figure';
492 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
493 $classes[] = "mw-halign-{$frameParams['align']}";
494 $caption = Html::rawElement(
495 'figcaption', [], $frameParams['caption'] ?? ''
496 );
497 } elseif ( isset( $frameParams['valign'] ) ) {
498 // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
499 // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
500 // mw-valign-text-bottom
501 $classes[] = "mw-valign-{$frameParams['valign']}";
502 }
503
504 if ( isset( $frameParams['border'] ) ) {
505 $classes[] = 'mw-image-border';
506 }
507
508 if ( isset( $frameParams['class'] ) ) {
509 $classes[] = $frameParams['class'];
510 }
511
512 $attribs = [
513 'class' => $classes,
514 'typeof' => $rdfaType,
515 ];
516
517 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
518
519 return str_replace( "\n", ' ', $s );
520 }
521
530 public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
531 $mtoParams = [];
532 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
533 $mtoParams['custom-url-link'] = $frameParams['link-url'];
534 if ( isset( $frameParams['link-target'] ) ) {
535 $mtoParams['custom-target-link'] = $frameParams['link-target'];
536 }
537 if ( $parser ) {
538 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
539 foreach ( $extLinkAttrs as $name => $val ) {
540 // Currently could include 'rel' and 'target'
541 $mtoParams['parser-extlink-' . $name] = $val;
542 }
543 }
544 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
545 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
546 $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
547 $linkRenderer->normalizeTarget( $frameParams['link-title'] )
548 );
549 if ( isset( $frameParams['link-title-query'] ) ) {
550 $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
551 }
552 } elseif ( !empty( $frameParams['no-link'] ) ) {
553 // No link
554 } else {
555 $mtoParams['desc-link'] = true;
556 $mtoParams['desc-query'] = $query;
557 }
558 return $mtoParams;
559 }
560
573 public static function makeThumbLinkObj(
574 LinkTarget $title, $file, $label = '', $alt = '', $align = null,
575 $params = [], $framed = false, $manualthumb = ''
576 ) {
577 $frameParams = [
578 'alt' => $alt,
579 'caption' => $label,
580 'align' => $align
581 ];
582 $classes = [];
583 if ( $manualthumb ) {
584 $frameParams['manualthumb'] = $manualthumb;
585 } elseif ( $framed ) {
586 $frameParams['framed'] = true;
587 } elseif ( !isset( $params['width'] ) ) {
588 $classes[] = 'mw-default-size';
589 }
591 $title, $file, $frameParams, $params, false, '', $classes
592 );
593 }
594
606 public static function makeThumbLink2(
607 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
608 $time = false, $query = '', array $classes = [], ?Parser $parser = null
609 ) {
610 $exists = $file && $file->exists();
611 $services = MediaWikiServices::getInstance();
612
613 $page = $handlerParams['page'] ?? false;
614 $lang = $handlerParams['lang'] ?? false;
615
616 if ( !isset( $frameParams['align'] ) ) {
617 $frameParams['align'] = '';
618 }
619 if ( !isset( $frameParams['caption'] ) ) {
620 $frameParams['caption'] = '';
621 }
622
623 if ( empty( $handlerParams['width'] ) ) {
624 // Reduce width for upright images when parameter 'upright' is used
625 $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
626 }
627
628 $thumb = false;
629 $noscale = false;
630 $manualthumb = false;
631 $manual_title = '';
632 $rdfaType = 'mw:File/Thumb';
633
634 if ( !$exists ) {
635 // Same precedence as the $exists case
636 if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
637 $rdfaType = 'mw:File/Frame';
638 }
639 $outerWidth = $handlerParams['width'] + 2;
640 } else {
641 if ( isset( $frameParams['manualthumb'] ) ) {
642 # Use manually specified thumbnail
643 $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
644 if ( $manual_title ) {
645 $manual_img = $services->getRepoGroup()
646 ->findFile( $manual_title );
647 if ( $manual_img ) {
648 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
649 $manualthumb = true;
650 }
651 }
652 } else {
653 $srcWidth = $file->getWidth( $page );
654 if ( isset( $frameParams['framed'] ) ) {
655 $rdfaType = 'mw:File/Frame';
656 if ( !$file->isVectorized() ) {
657 // Use image dimensions, don't scale
658 $noscale = true;
659 } else {
660 // framed is unscaled, but for vectorized images
661 // we need to a width for scaling up for the high density variants
662 $handlerParams['width'] = $srcWidth;
663 }
664 }
665
666 // Do not present an image bigger than the source, for bitmap-style images
667 // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
668 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
669 $handlerParams['width'] = $srcWidth;
670 }
671
672 $thumb = $noscale
673 ? $file->getUnscaledThumb( $handlerParams )
674 : $file->transform( $handlerParams );
675 }
676
677 if ( $thumb ) {
678 $outerWidth = $thumb->getWidth() + 2;
679 } else {
680 $outerWidth = $handlerParams['width'] + 2;
681 }
682 }
683
684 if ( $parser && $rdfaType === 'mw:File/Thumb' ) {
685 $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
686 }
687
688 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
689 $linkTitleQuery = [];
690 if ( $page || $lang ) {
691 if ( $page ) {
692 $linkTitleQuery['page'] = $page;
693 }
694 if ( $lang ) {
695 $linkTitleQuery['lang'] = $lang;
696 }
697 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
698 # So we don't need to pass it here in $query. However, the URL for the
699 # zoom icon still needs it, so we make a unique query for it. See T16771
700 $url = wfAppendQuery( $url, $linkTitleQuery );
701 }
702
703 if ( $manualthumb
704 && !isset( $frameParams['link-title'] )
705 && !isset( $frameParams['link-url'] )
706 && !isset( $frameParams['no-link'] ) ) {
707 $frameParams['link-title'] = $title;
708 $frameParams['link-title-query'] = $linkTitleQuery;
709 }
710
711 if ( $frameParams['align'] != '' ) {
712 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
713 $classes[] = "mw-halign-{$frameParams['align']}";
714 }
715
716 if ( isset( $frameParams['class'] ) ) {
717 $classes[] = $frameParams['class'];
718 }
719
720 $s = '';
721
722 $isBadFile = $exists && $thumb && $parser &&
723 $parser->getBadFileLookup()->isBadFile(
724 $manualthumb ? $manual_title : $title->getDBkey(),
725 $parser->getTitle()
726 );
727
728 if ( !$exists ) {
729 $rdfaType = 'mw:Error ' . $rdfaType;
730 $label = $frameParams['alt'] ?? '';
732 $title, $label, '', '', '', (bool)$time, $handlerParams, false
733 );
734 $zoomIcon = '';
735 } elseif ( !$thumb || $thumb->isError() || $isBadFile ) {
736 $rdfaType = 'mw:Error ' . $rdfaType;
737 if ( $thumb && $thumb->isError() ) {
738 Assert::invariant(
739 $thumb instanceof MediaTransformError,
740 'Unknown MediaTransformOutput: ' . get_class( $thumb )
741 );
742 $label = $thumb->toText();
743 } elseif ( !$thumb ) {
744 $label = wfMessage( 'thumbnail_error', '' )->text();
745 } else {
746 $label = '';
747 }
749 $title, $label, '', '', '', (bool)$time, $handlerParams, true
750 );
751 $zoomIcon = '';
752 } else {
753 if ( !$noscale && !$manualthumb ) {
754 self::processResponsiveImages( $file, $thumb, $handlerParams );
755 }
756 $params = [];
757 // An empty alt indicates an image is not a key part of the content
758 // and that non-visual browsers may omit it from rendering. Only
759 // set the parameter if it's explicitly requested.
760 if ( isset( $frameParams['alt'] ) ) {
761 $params['alt'] = $frameParams['alt'];
762 }
763 $params += [
764 'img-class' => 'mw-file-element',
765 ];
766 // Only thumbs gets the magnify link
767 if ( $rdfaType === 'mw:File/Thumb' ) {
768 $params['magnify-resource'] = $url;
769 }
770 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
771 $s .= $thumb->toHtml( $params );
772 if ( isset( $frameParams['framed'] ) ) {
773 $zoomIcon = '';
774 } else {
775 $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
776 Html::rawElement( 'a', [
777 'href' => $url,
778 'class' => 'internal',
779 'title' => wfMessage( 'thumbnail-more' )->text(),
780 ] )
781 );
782 }
783 }
784
785 $s .= Html::rawElement(
786 'figcaption', [], $frameParams['caption'] ?? ''
787 );
788
789 $attribs = [
790 'class' => $classes,
791 'typeof' => $rdfaType,
792 ];
793
794 $s = Html::rawElement( 'figure', $attribs, $s );
795
796 return str_replace( "\n", ' ', $s );
797 }
798
807 public static function processResponsiveImages( $file, $thumb, $hp ) {
808 $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
809 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
810 $hp15 = $hp;
811 $hp15['width'] = round( $hp['width'] * 1.5 );
812 $hp20 = $hp;
813 $hp20['width'] = $hp['width'] * 2;
814 if ( isset( $hp['height'] ) ) {
815 $hp15['height'] = round( $hp['height'] * 1.5 );
816 $hp20['height'] = $hp['height'] * 2;
817 }
818
819 $thumb15 = $file->transform( $hp15 );
820 $thumb20 = $file->transform( $hp20 );
821 if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
822 $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
823 }
824 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
825 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
826 }
827 }
828 }
829
844 public static function makeBrokenImageLinkObj(
845 $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
846 $time = false, array $handlerParams = [], bool $currentExists = false
847 ) {
848 if ( !$title instanceof LinkTarget ) {
849 wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
850 return "<!-- ERROR -->" . htmlspecialchars( $label );
851 }
852
853 $title = Title::newFromLinkTarget( $title );
854 $services = MediaWikiServices::getInstance();
855 $mainConfig = $services->getMainConfig();
856 $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
857 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
858 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
859 if ( $label == '' ) {
860 $label = $title->getPrefixedText();
861 }
862
863 $html = Html::element( 'span', [
864 'class' => 'mw-file-element mw-broken-media',
865 // These data attributes are used to dynamically size the span, see T273013
866 'data-width' => $handlerParams['width'] ?? null,
867 'data-height' => $handlerParams['height'] ?? null,
868 ], $label );
869
870 $repoGroup = $services->getRepoGroup();
871 $currentExists = $currentExists ||
872 ( $time && $repoGroup->findFile( $title ) !== false );
873
874 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
875 && !$currentExists
876 ) {
877 if (
878 $title->inNamespace( NS_FILE ) &&
879 $repoGroup->getLocalRepo()->checkRedirect( $title )
880 ) {
881 // We already know it's a redirect, so mark it accordingly
882 return self::link(
883 $title,
884 $html,
885 [ 'class' => 'mw-redirect' ],
886 wfCgiToArray( $query ),
887 [ 'known', 'noclasses' ]
888 );
889 }
890 return Html::rawElement( 'a', [
891 'href' => self::getUploadUrl( $title, $query ),
892 'class' => 'new',
893 'title' => $title->getPrefixedText()
894 ], $html );
895 }
896 return self::link(
897 $title,
898 $html,
899 [],
900 wfCgiToArray( $query ),
901 [ 'known', 'noclasses' ]
902 );
903 }
904
913 public static function getUploadUrl( $destFile, $query = '' ) {
914 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
915 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
916 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
917 $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
918 if ( $query != '' ) {
919 $q .= '&' . $query;
920 }
921
922 if ( $uploadMissingFileUrl ) {
923 return wfAppendQuery( $uploadMissingFileUrl, $q );
924 }
925
926 if ( $uploadNavigationUrl ) {
927 return wfAppendQuery( $uploadNavigationUrl, $q );
928 }
929
930 $upload = SpecialPage::getTitleFor( 'Upload' );
931
932 return $upload->getLocalURL( $q );
933 }
934
944 public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
945 $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
946 $title, [ 'time' => $time ]
947 );
948 return self::makeMediaLinkFile( $title, $img, $html );
949 }
950
963 public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
964 if ( $file && $file->exists() ) {
965 $url = $file->getUrl();
966 $class = 'internal';
967 } else {
968 $url = self::getUploadUrl( $title );
969 $class = 'new';
970 }
971
972 $alt = $title->getText();
973 if ( $html == '' ) {
974 $html = $alt;
975 }
976
977 $ret = '';
978 $attribs = [
979 'href' => $url,
980 'class' => $class,
981 'title' => $alt
982 ];
983
984 if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
985 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
986 ) {
987 wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
988 . "with url {$url} and text {$html} to {$ret}" );
989 return $ret;
990 }
991
992 return Html::rawElement( 'a', $attribs, $html );
993 }
994
1005 public static function specialLink( $name, $key = '' ) {
1006 $queryPos = strpos( $name, '?' );
1007 if ( $queryPos !== false ) {
1008 $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
1009 $name = substr( $name, 0, $queryPos );
1010 } else {
1011 $getParams = [];
1012 }
1013
1014 $slashPos = strpos( $name, '/' );
1015 if ( $slashPos !== false ) {
1016 $subpage = substr( $name, $slashPos + 1 );
1017 $name = substr( $name, 0, $slashPos );
1018 } else {
1019 $subpage = false;
1020 }
1021
1022 if ( $key == '' ) {
1023 $key = strtolower( $name );
1024 }
1025
1026 return self::linkKnown(
1027 SpecialPage::getTitleFor( $name, $subpage ),
1028 wfMessage( $key )->escaped(),
1029 [],
1030 $getParams
1031 );
1032 }
1033
1054 public static function makeExternalLink( $url, $text, $escape = true,
1055 $linktype = '', $attribs = [], $title = null
1056 ) {
1057 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1058 global $wgTitle;
1059 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1060 return $linkRenderer->makeExternalLink(
1061 $url,
1062 $escape ? $text : new HtmlArmor( $text ),
1063 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1064 $linktype,
1065 $attribs
1066 );
1067 }
1068
1083 public static function userLink(
1084 $userId,
1085 $userName,
1086 $altUserName = false,
1087 $attributes = []
1088 ) {
1089 if ( $userName === '' || $userName === false || $userName === null ) {
1090 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1091 'that need to be fixed?' );
1092 return wfMessage( 'empty-username' )->parse();
1093 }
1094
1095 return MediaWikiServices::getInstance()->getUserLinkRenderer()
1096 ->userLink(
1097 new UserIdentityValue( $userId, (string)$userName ),
1098 RequestContext::getMain(),
1099 $altUserName === false ? null : (string)$altUserName,
1100 $attributes
1101 );
1102 }
1103
1122 public static function userToolLinkArray(
1123 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1124 ): array {
1125 $services = MediaWikiServices::getInstance();
1126 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1127 $talkable = !( $disableAnonTalk && $userId == 0 );
1128 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1129 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1130
1131 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1132 // No tools for an external user
1133 return [];
1134 }
1135
1136 $items = [];
1137 if ( $talkable ) {
1138 $items[] = self::userTalkLink( $userId, $userText );
1139 }
1140 if ( $userId ) {
1141 // check if the user has an edit
1142 $attribs = [];
1143 $attribs['class'] = 'mw-usertoollinks-contribs';
1144 if ( $redContribsWhenNoEdits ) {
1145 if ( $edits === null ) {
1146 $user = UserIdentityValue::newRegistered( $userId, $userText );
1147 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1148 }
1149 if ( $edits === 0 ) {
1150 // Note: "new" class is inappropriate here, as "new" class
1151 // should only be used for pages that do not exist.
1152 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1153 }
1154 }
1155 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1156
1157 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1158 }
1159 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1160 if ( $blockable && $userCanBlock ) {
1161 $items[] = self::blockLink( $userId, $userText );
1162 }
1163
1164 if (
1165 $addEmailLink
1166 && MediaWikiServices::getInstance()->getEmailUserFactory()
1167 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1168 ->canSend()
1169 ->isGood()
1170 ) {
1171 $items[] = self::emailLink( $userId, $userText );
1172 }
1173
1174 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1175
1176 return $items;
1177 }
1178
1186 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1187 global $wgLang;
1188
1189 if ( !$items ) {
1190 return '';
1191 }
1192
1193 if ( $useParentheses ) {
1194 return wfMessage( 'word-separator' )->escaped()
1195 . '<span class="mw-usertoollinks">'
1196 . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1197 . '</span>';
1198 }
1199
1200 $tools = [];
1201 foreach ( $items as $tool ) {
1202 $tools[] = Html::rawElement( 'span', [], $tool );
1203 }
1204 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1205 implode( ' ', $tools ) . '</span>';
1206 }
1207
1222 public static function userToolLinks(
1223 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1224 $useParentheses = true
1225 ) {
1226 if ( $userText === '' ) {
1227 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1228 'that need to be fixed?' );
1229 return ' ' . wfMessage( 'empty-username' )->parse();
1230 }
1231
1232 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1233 return self::renderUserToolLinksArray( $items, $useParentheses );
1234 }
1235
1245 public static function userToolLinksRedContribs(
1246 $userId, $userText, $edits = null, $useParentheses = true
1247 ) {
1248 return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1249 }
1250
1257 public static function userTalkLink( $userId, $userText ) {
1258 if ( $userText === '' ) {
1259 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1260 'that need to be fixed?' );
1261 return wfMessage( 'empty-username' )->parse();
1262 }
1263
1264 $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1265 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1266 $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1267
1268 return $userTalkPage
1269 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1270 : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1271 }
1272
1279 public static function blockLink( $userId, $userText ) {
1280 if ( $userText === '' ) {
1281 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1282 'that need to be fixed?' );
1283 return wfMessage( 'empty-username' )->parse();
1284 }
1285
1286 $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1287 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1288
1289 return self::link( $blockPage,
1290 wfMessage( 'blocklink' )->escaped(),
1291 $moreLinkAttribs
1292 );
1293 }
1294
1300 public static function emailLink( $userId, $userText ) {
1301 if ( $userText === '' ) {
1302 wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1303 'that need to be fixed?' );
1304 return wfMessage( 'empty-username' )->parse();
1305 }
1306
1307 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1308 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1309 return self::link( $emailPage,
1310 wfMessage( 'emaillink' )->escaped(),
1311 $moreLinkAttribs
1312 );
1313 }
1314
1326 public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1327 // TODO inject authority
1328 $authority = RequestContext::getMain()->getAuthority();
1329
1330 $revUser = $revRecord->getUser(
1331 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1332 $authority
1333 );
1334 if ( $revUser ) {
1335 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1336 } else {
1337 // User is deleted and we can't (or don't want to) view it
1338 $link = wfMessage( 'rev-deleted-user' )->escaped();
1339 }
1340
1341 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1342 $class = self::getRevisionDeletedClass( $revRecord );
1343 return '<span class="' . $class . '">' . $link . '</span>';
1344 }
1345 return $link;
1346 }
1347
1354 public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1355 $class = 'history-deleted';
1356 if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1357 $class .= ' mw-history-suppressed';
1358 }
1359 return $class;
1360 }
1361
1374 public static function revUserTools(
1375 RevisionRecord $revRecord,
1376 $isPublic = false,
1377 $useParentheses = true
1378 ) {
1379 // TODO inject authority
1380 $authority = RequestContext::getMain()->getAuthority();
1381
1382 $revUser = $revRecord->getUser(
1383 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1384 $authority
1385 );
1386 if ( $revUser ) {
1387 $link = self::userLink(
1388 $revUser->getId(),
1389 $revUser->getName(),
1390 false,
1391 [ 'data-mw-revid' => $revRecord->getId() ]
1392 ) . self::userToolLinks(
1393 $revUser->getId(),
1394 $revUser->getName(),
1395 false,
1396 0,
1397 null,
1398 $useParentheses
1399 );
1400 } else {
1401 // User is deleted and we can't (or don't want to) view it
1402 $link = wfMessage( 'rev-deleted-user' )->escaped();
1403 }
1404
1405 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1406 $class = self::getRevisionDeletedClass( $revRecord );
1407 return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1408 }
1409 return $link;
1410 }
1411
1422 public static function expandLocalLinks( string $html ) {
1423 return HtmlHelper::modifyElements(
1424 $html,
1425 static function ( SerializerNode $node ): bool {
1426 return $node->name === 'a' && isset( $node->attrs['href'] );
1427 },
1428 static function ( SerializerNode $node ): SerializerNode {
1429 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1430 $node->attrs['href'] =
1431 $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE ) ?? false;
1432 return $node;
1433 }
1434 );
1435 }
1436
1443 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1444 # Valid link forms:
1445 # Foobar -- normal
1446 # :Foobar -- override special treatment of prefix (images, language links)
1447 # /Foobar -- convert to CurrentPage/Foobar
1448 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1449 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1450 # ../Foobar -- convert to CurrentPage/Foobar,
1451 # (from CurrentPage/CurrentSubPage)
1452 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1453 # (from CurrentPage/CurrentSubPage)
1454
1455 $ret = $target; # default return value is no change
1456
1457 # Some namespaces don't allow subpages,
1458 # so only perform processing if subpages are allowed
1459 if (
1460 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1461 hasSubpages( $contextTitle->getNamespace() )
1462 ) {
1463 $hash = strpos( $target, '#' );
1464 if ( $hash !== false ) {
1465 $suffix = substr( $target, $hash );
1466 $target = substr( $target, 0, $hash );
1467 } else {
1468 $suffix = '';
1469 }
1470 # T9425
1471 $target = trim( $target );
1472 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1473 getPrefixedText( $contextTitle );
1474 # Look at the first character
1475 if ( $target != '' && $target[0] === '/' ) {
1476 # / at end means we don't want the slash to be shown
1477 $m = [];
1478 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1479 if ( $trailingSlashes ) {
1480 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1481 } else {
1482 $noslash = substr( $target, 1 );
1483 }
1484
1485 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1486 if ( $text === '' ) {
1487 $text = $target . $suffix;
1488 } # this might be changed for ugliness reasons
1489 } else {
1490 # check for .. subpage backlinks
1491 $dotdotcount = 0;
1492 $nodotdot = $target;
1493 while ( str_starts_with( $nodotdot, '../' ) ) {
1494 ++$dotdotcount;
1495 $nodotdot = substr( $nodotdot, 3 );
1496 }
1497 if ( $dotdotcount > 0 ) {
1498 $exploded = explode( '/', $contextPrefixedText );
1499 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1500 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1501 # / at the end means don't show full path
1502 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1503 $nodotdot = rtrim( $nodotdot, '/' );
1504 if ( $text === '' ) {
1505 $text = $nodotdot . $suffix;
1506 }
1507 }
1508 $nodotdot = trim( $nodotdot );
1509 if ( $nodotdot != '' ) {
1510 $ret .= '/' . $nodotdot;
1511 }
1512 $ret .= $suffix;
1513 }
1514 }
1515 }
1516 }
1517
1518 return $ret;
1519 }
1520
1526 public static function formatRevisionSize( $size ) {
1527 if ( $size == 0 ) {
1528 $stxt = wfMessage( 'historyempty' )->escaped();
1529 } else {
1530 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1531 }
1532 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1533 }
1534
1541 public static function splitTrail( $trail ) {
1542 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1543 $inside = '';
1544 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1545 [ , $inside, $trail ] = $m;
1546 }
1547 return [ $inside, $trail ];
1548 }
1549
1580 public static function generateRollback(
1581 RevisionRecord $revRecord,
1582 ?IContextSource $context = null,
1583 $options = []
1584 ) {
1585 $context ??= RequestContext::getMain();
1586
1587 $editCount = self::getRollbackEditCount( $revRecord );
1588 if ( $editCount === false ) {
1589 return '';
1590 }
1591
1592 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1593
1594 $services = MediaWikiServices::getInstance();
1595 // Allow extensions to modify the rollback link.
1596 // Abort further execution if the extension wants full control over the link.
1597 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1598 $revRecord, $context, $options, $inner ) ) {
1599 return $inner;
1600 }
1601
1602 if ( !in_array( 'noBrackets', $options, true ) ) {
1603 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1604 }
1605
1606 if ( $services->getUserOptionsLookup()
1607 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1608 ) {
1609 $services->getStatsFactory()
1610 ->getCounter( 'rollbackconfirmation_event_load_total' )
1611 ->copyToStatsdAt( 'rollbackconfirmation.event.load' )
1612 ->increment();
1613 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1614 }
1615
1616 return '<span class="mw-rollback-link">' . $inner . '</span>';
1617 }
1618
1637 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1638 if ( func_num_args() > 1 ) {
1639 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1640 }
1641 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1642 ->get( MainConfigNames::ShowRollbackEditCount );
1643
1644 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1645 // Nothing has happened, indicate this by returning 'null'
1646 return null;
1647 }
1648
1649 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1650
1651 // Up to the value of $wgShowRollbackEditCount revisions are counted
1652 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1653 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1654 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1655 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1656 ->limit( $showRollbackEditCount + 1 )
1657 ->caller( __METHOD__ )->fetchResultSet();
1658
1659 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1660 $revUserText = $revUser ? $revUser->getName() : '';
1661
1662 $editCount = 0;
1663 $moreRevs = false;
1664 foreach ( $res as $row ) {
1665 if ( $row->rev_user_text != $revUserText ) {
1666 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1667 || $row->rev_deleted & RevisionRecord::DELETED_USER
1668 ) {
1669 // If the user or the text of the revision we might rollback
1670 // to is deleted in some way we can't rollback. Similar to
1671 // the checks in WikiPage::commitRollback.
1672 return false;
1673 }
1674 $moreRevs = true;
1675 break;
1676 }
1677 $editCount++;
1678 }
1679
1680 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1681 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1682 // and there weren't any other revisions. That means that the current user is the only
1683 // editor, so we can't rollback
1684 return false;
1685 }
1686 return $editCount;
1687 }
1688
1703 public static function buildRollbackLink(
1704 RevisionRecord $revRecord,
1705 ?IContextSource $context = null,
1706 $editCount = false
1707 ) {
1708 $config = MediaWikiServices::getInstance()->getMainConfig();
1709 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1710 $miserMode = $config->get( MainConfigNames::MiserMode );
1711 // To config which pages are affected by miser mode
1712 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1713
1714 $context ??= RequestContext::getMain();
1715
1716 $title = $revRecord->getPageAsLinkTarget();
1717 $revUser = $revRecord->getUser();
1718 $revUserText = $revUser ? $revUser->getName() : '';
1719
1720 $query = [
1721 'action' => 'rollback',
1722 'from' => $revUserText,
1723 'token' => $context->getUser()->getEditToken( 'rollback' ),
1724 ];
1725
1726 $attrs = [
1727 'data-mw' => 'interface',
1728 'title' => $context->msg( 'tooltip-rollback' )->text()
1729 ];
1730
1731 $options = [ 'known', 'noclasses' ];
1732
1733 if ( $context->getRequest()->getBool( 'bot' ) ) {
1734 // T17999
1735 $query['hidediff'] = '1';
1736 $query['bot'] = '1';
1737 }
1738
1739 if ( $miserMode ) {
1740 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1741 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1742 $showRollbackEditCount = false;
1743 break;
1744 }
1745 }
1746 }
1747
1748 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1749 $msg = [ 'rollbacklink' ];
1750 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1751 if ( !is_numeric( $editCount ) ) {
1752 $editCount = self::getRollbackEditCount( $revRecord );
1753 }
1754
1755 if ( $editCount > $showRollbackEditCount ) {
1756 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1757 } elseif ( $editCount ) {
1758 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1759 }
1760 }
1761
1762 $html = $context->msg( ...$msg )->parse();
1763 return self::link( $title, $html, $attrs, $query, $options );
1764 }
1765
1774 public static function formatHiddenCategories( $hiddencats ) {
1775 $outText = '';
1776 if ( count( $hiddencats ) > 0 ) {
1777 # Construct the HTML
1778 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1779 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1780 $outText .= "</div><ul>\n";
1781
1782 foreach ( $hiddencats as $titleObj ) {
1783 # If it's hidden, it must exist - no need to check with a LinkBatch
1784 $outText .= '<li>'
1785 . self::link( $titleObj, null, [], [], 'known' )
1786 . "</li>\n";
1787 }
1788 $outText .= '</ul>';
1789 }
1790 return $outText;
1791 }
1792
1796 private static function getContextFromMain() {
1797 $context = RequestContext::getMain();
1798 $context = new DerivativeContext( $context );
1799 return $context;
1800 }
1801
1819 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1820 if ( !$localizer ) {
1821 $localizer = self::getContextFromMain();
1822 }
1823 $message = $localizer->msg( "tooltip-$name", $msgParams );
1824 // Set a default tooltip for subject namespace tabs if that hasn't
1825 // been defined. See T22126
1826 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1827 $message = $localizer->msg( 'tooltip-ca-nstab' );
1828 }
1829
1830 if ( $message->isDisabled() ) {
1831 $tooltip = false;
1832 } else {
1833 $tooltip = $message->text();
1834 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1835 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1836 }
1837
1838 $options = (array)$options;
1839
1840 if ( in_array( 'nonexisting', $options ) ) {
1841 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1842 }
1843 if ( in_array( 'withaccess', $options ) ) {
1844 $accesskey = self::accesskey( $name, $localizer );
1845 if ( $accesskey !== false ) {
1846 // Should be build the same as in jquery.accessKeyLabel.js
1847 if ( $tooltip === false || $tooltip === '' ) {
1848 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1849 } else {
1850 $tooltip .= $localizer->msg( 'word-separator' )->text();
1851 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1852 }
1853 }
1854 }
1855
1856 return $tooltip;
1857 }
1858
1860 public static $accesskeycache;
1861
1874 public static function accesskey( $name, $localizer = null ) {
1875 if ( !isset( self::$accesskeycache[$name] ) ) {
1876 if ( !$localizer ) {
1877 $localizer = self::getContextFromMain();
1878 }
1879 $msg = $localizer->msg( "accesskey-$name" );
1880 // Set a default accesskey for subject namespace tabs if an
1881 // accesskey has not been defined. See T22126
1882 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1883 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1884 }
1885 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1886 }
1887 return self::$accesskeycache[$name];
1888 }
1889
1904 public static function getRevDeleteLink(
1905 Authority $performer,
1906 RevisionRecord $revRecord,
1907 LinkTarget $title
1908 ) {
1909 $canHide = $performer->isAllowed( 'deleterevision' );
1910 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
1911 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
1912 return '';
1913 }
1914
1915 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
1916 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
1917 }
1918 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
1919 getPrefixedDBkey( $title );
1920 if ( $revRecord->getId() ) {
1921 // RevDelete links using revision ID are stable across
1922 // page deletion and undeletion; use when possible.
1923 $query = [
1924 'type' => 'revision',
1925 'target' => $prefixedDbKey,
1926 'ids' => $revRecord->getId()
1927 ];
1928 } else {
1929 // Older deleted entries didn't save a revision ID.
1930 // We have to refer to these by timestamp, ick!
1931 $query = [
1932 'type' => 'archive',
1933 'target' => $prefixedDbKey,
1934 'ids' => $revRecord->getTimestamp()
1935 ];
1936 }
1937 return self::revDeleteLink(
1938 $query,
1939 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
1940 $canHide
1941 );
1942 }
1943
1956 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
1957 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
1958 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1959 $html = wfMessage( $msgKey )->escaped();
1960 $tag = $restricted ? 'strong' : 'span';
1961 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
1962 return Html::rawElement(
1963 $tag,
1964 [ 'class' => 'mw-revdelundel-link' ],
1965 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
1966 );
1967 }
1968
1980 public static function revDeleteLinkDisabled( $delete = true ) {
1981 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1982 $html = wfMessage( $msgKey )->escaped();
1983 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
1984 return Html::rawElement( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
1985 }
1986
2000 public static function tooltipAndAccesskeyAttribs(
2001 $name,
2002 array $msgParams = [],
2003 $options = null,
2004 $localizer = null
2005 ) {
2006 $options = (array)$options;
2007 $options[] = 'withaccess';
2008
2009 // Get optional parameters from global context if any missing.
2010 if ( !$localizer ) {
2011 $localizer = self::getContextFromMain();
2012 }
2013
2014 $attribs = [
2015 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2016 'accesskey' => self::accesskey( $name, $localizer )
2017 ];
2018 if ( $attribs['title'] === false ) {
2019 unset( $attribs['title'] );
2020 }
2021 if ( $attribs['accesskey'] === false ) {
2022 unset( $attribs['accesskey'] );
2023 }
2024 return $attribs;
2025 }
2026
2034 public static function tooltip( $name, $options = null ) {
2035 $tooltip = self::titleAttrib( $name, $options );
2036 if ( $tooltip === false ) {
2037 return '';
2038 }
2039 return Html::expandAttributes( [
2040 'title' => $tooltip
2041 ] );
2042 }
2043
2044}
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:559
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:559
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:1422
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:1956
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:1860
static blockLink( $userId, $userText)
Definition Linker.php:1279
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:2000
static getUploadUrl( $destFile, $query='')
Get the URL to upload a certain file.
Definition Linker.php:913
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:944
static processResponsiveImages( $file, $thumb, $hp)
Process responsive images: add 1.5x and 2x subimages to the thumbnail, where applicable.
Definition Linker.php:807
static userTalkLink( $userId, $userText)
Definition Linker.php:1257
static generateRollback(RevisionRecord $revRecord, ?IContextSource $context=null, $options=[])
Generate a rollback link for a given revision.
Definition Linker.php:1580
static buildRollbackLink(RevisionRecord $revRecord, ?IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1703
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1443
static specialLink( $name, $key='')
Make a link to a special page given its name and, optionally, a message key from the link text.
Definition Linker.php:1005
static userToolLinks( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null, $useParentheses=true)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1222
static emailLink( $userId, $userText)
Definition Linker.php:1300
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1774
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition Linker.php: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:1637
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1054
static userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1122
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition Linker.php:530
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:1904
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query='', array $classes=[], ?Parser $parser=null)
Definition Linker.php:606
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:2034
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[], bool $currentExists=false)
Make a "broken" link to an image.
Definition Linker.php:844
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:963
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1874
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:1819
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1186
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition Linker.php:1245
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:1541
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:1980
static formatRevisionSize( $size)
Definition Linker.php:1526
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:573
static userLink( $userId, $userName, $altUserName=false, $attributes=[])
Make user link (or user contributions for unregistered users)
Definition Linker.php:1083
static revUserLink(RevisionRecord $revRecord, $isPublic=false)
Generate a user link if the current user is allowed to view it.
Definition Linker.php:1326
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1354
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:1374
A class containing constants representing the names of configuration variables.
const UploadNavigationUrl
Name constant for the UploadNavigationUrl setting, for use with Config::get()
const ThumbUpright
Name constant for the ThumbUpright setting, for use with Config::get()
const EnableUploads
Name constant for the EnableUploads setting, for use with Config::get()
const SVGMaxSize
Name constant for the SVGMaxSize setting, for use with Config::get()
const ResponsiveImages
Name constant for the ResponsiveImages setting, for use with Config::get()
const DisableAnonTalk
Name constant for the DisableAnonTalk setting, for use with Config::get()
const ThumbLimits
Name constant for the ThumbLimits setting, for use with Config::get()
const UploadMissingFileUrl
Name constant for the UploadMissingFileUrl setting, for use with Config::get()
Service locator for MediaWiki core services.
getMainConfig()
Returns the Config object that provides configuration for MediaWiki core.
static getInstance()
Returns the global default instance of the top level service locator.
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:1267
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)