MediaWiki master
Linker.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Linker;
10
33use Wikimedia\Assert\Assert;
36use Wikimedia\RemexHtml\Serializer\SerializerNode;
37
47class Linker {
51 public const TOOL_LINKS_NOBLOCK = 1;
52 public const TOOL_LINKS_EMAIL = 2;
53
95 public static function link(
96 $target, $html = null, $customAttribs = [], $query = [], $options = []
97 ) {
98 if ( !$target instanceof LinkTarget ) {
99 wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
100 return "<!-- ERROR -->$html";
101 }
102
103 $options = (array)$options;
104 $linkRenderer = self::getLinkRenderer( $options );
105
106 if ( $html !== null ) {
107 $text = new HtmlArmor( $html );
108 } else {
109 $text = null;
110 }
111
112 if ( in_array( 'known', $options, true ) ) {
113 return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
114 }
115
116 if ( in_array( 'broken', $options, true ) ) {
117 return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
118 }
119
120 if ( in_array( 'noclasses', $options, true ) ) {
121 return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query );
122 }
123
124 return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
125 }
126
146 public static function linkKnown(
147 $target, $html = null, $customAttribs = [],
148 $query = [], $options = [ 'known' ]
149 ) {
150 return self::link( $target, $html, $customAttribs, $query, $options );
151 }
152
170 public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '', $hash = '' ) {
171 $nt = Title::newFromLinkTarget( $nt );
172 $attrs = [];
173 if ( $hash ) {
174 $attrs['class'] = 'mw-selflink-fragment';
175 $attrs['href'] = '#' . $hash;
176 } else {
177 // For backwards compatibility with gadgets we add selflink as well.
178 $attrs['class'] = 'mw-selflink selflink';
179 }
180 $ret = Html::rawElement( 'a', $attrs, $prefix . $html ) . $trail;
181 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
182 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
183 return $ret;
184 }
185
186 if ( $html == '' ) {
187 $html = htmlspecialchars( $nt->getPrefixedText() );
188 }
189 [ $inside, $trail ] = self::splitTrail( $trail );
190 return Html::rawElement( 'a', $attrs, $prefix . $html . $inside ) . $trail;
191 }
192
203 public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
204 // First we check whether the namespace exists or not.
205 if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
206 if ( $namespace == NS_MAIN ) {
207 $name = $context->msg( 'blanknamespace' )->text();
208 } else {
209 $name = MediaWikiServices::getInstance()->getContentLanguage()->
210 getFormattedNsText( $namespace );
211 }
212 return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
213 }
214
215 return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
216 }
217
226 private static function fnamePart( $url ) {
227 $basename = strrchr( $url, '/' );
228 if ( $basename === false ) {
229 $basename = $url;
230 } else {
231 $basename = substr( $basename, 1 );
232 }
233 return $basename;
234 }
235
246 public static function makeExternalImage( $url, $alt = '' ) {
247 if ( $alt == '' ) {
248 $alt = self::fnamePart( $url );
249 }
250 $img = '';
251 $success = ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
252 ->onLinkerMakeExternalImage( $url, $alt, $img );
253 if ( !$success ) {
254 wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
255 . "with url {$url} and alt text {$alt} to {$img}" );
256 return $img;
257 }
258 return Html::element( 'img',
259 [
260 'src' => $url,
261 'alt' => $alt
262 ]
263 );
264 }
265
304 public static function makeImageLink( Parser $parser, LinkTarget $title,
305 $file, $frameParams = [], $handlerParams = [], $time = false,
306 $query = '', $widthOption = null
307 ) {
308 $title = Title::newFromLinkTarget( $title );
309 $res = null;
310 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
311 if ( !$hookRunner->onImageBeforeProduceHTML( null, $title,
312 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
313 $file, $frameParams, $handlerParams, $time, $res,
314 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
315 $parser, $query, $widthOption )
316 ) {
317 return $res;
318 }
319
320 if ( $file && !$file->allowInlineDisplay() ) {
321 wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ' does not allow inline display' );
322 return self::link( $title );
323 }
324
325 // Clean up parameters
326 $page = $handlerParams['page'] ?? false;
327 if ( !isset( $frameParams['align'] ) ) {
328 $frameParams['align'] = '';
329 }
330 if ( !isset( $frameParams['title'] ) ) {
331 $frameParams['title'] = '';
332 }
333 if ( !isset( $frameParams['class'] ) ) {
334 $frameParams['class'] = '';
335 }
336
337 $services = MediaWikiServices::getInstance();
338 $config = $services->getMainConfig();
339
340 $classes = [];
341 if (
342 !isset( $handlerParams['width'] ) &&
343 !isset( $frameParams['manualthumb'] ) &&
344 !isset( $frameParams['framed'] )
345 ) {
346 $classes[] = 'mw-default-size';
347 }
348
349 $prefix = $postfix = '';
350
351 if ( $file && !isset( $handlerParams['width'] ) ) {
352 if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
353 // If its a vector image, and user only specifies height
354 // we don't want it to be limited by its "normal" width.
355 $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
356 $handlerParams['width'] = $svgMaxSize;
357 } else {
358 $handlerParams['width'] = $file->getWidth( $page );
359 }
360
361 if ( isset( $frameParams['thumbnail'] )
362 || isset( $frameParams['manualthumb'] )
363 || isset( $frameParams['framed'] )
364 || isset( $frameParams['frameless'] )
365 || !$handlerParams['width']
366 ) {
367 $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
368 $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
369 if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
370 $userOptionsLookup = $services->getUserOptionsLookup();
371 $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
372 }
373
374 // Reduce width for upright images when parameter 'upright' is used
375 if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
376 $frameParams['upright'] = $thumbUpright;
377 }
378
379 // For caching health: If width scaled down due to upright
380 // parameter, round to full __0 pixel to avoid the creation of a
381 // lot of odd thumbs.
382 $prefWidth = isset( $frameParams['upright'] ) ?
383 round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
384 $thumbLimits[$widthOption];
385
386 // Use width which is smaller: real image width or user preference width
387 // Unless image is scalable vector.
388 if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
389 $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
390 $handlerParams['width'] = $prefWidth;
391 }
392 }
393 }
394
395 // Parser::makeImage has a similarly named variable
396 $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
397 isset( $frameParams['manualthumb'] ) ||
398 isset( $frameParams['framed'] );
399
400 if ( $hasVisibleCaption ) {
401 return $prefix . self::makeThumbLink2(
402 $title, $file, $frameParams, $handlerParams, $time, $query,
403 $classes, $parser
404 ) . $postfix;
405 }
406
407 $rdfaType = 'mw:File';
408
409 if ( isset( $frameParams['frameless'] ) ) {
410 $rdfaType .= '/Frameless';
411 if ( $file ) {
412 $srcWidth = $file->getWidth( $page );
413 # For "frameless" option: do not present an image bigger than the
414 # source (for bitmap-style images). This is the same behavior as the
415 # "thumb" option does it already.
416 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
417 $handlerParams['width'] = $srcWidth;
418 }
419 }
420 }
421
422 if ( $file && isset( $handlerParams['width'] ) ) {
423 # Create a resized image, without the additional thumbnail features
424 $thumb = $file->transform( $handlerParams );
425 } else {
426 $thumb = false;
427 }
428
429 $isBadFile = $file && $thumb &&
430 $parser->getBadFileLookup()->isBadFile( $title->getDBkey(), $parser->getTitle() );
431
432 if ( !$thumb || $thumb->isError() || $isBadFile ) {
433 $rdfaType = 'mw:Error ' . $rdfaType;
434 $currentExists = $file && $file->exists();
435 if ( $currentExists && !$thumb ) {
436 $label = wfMessage( 'thumbnail_error', '' )->text();
437 } elseif ( $thumb && $thumb->isError() ) {
438 Assert::invariant(
439 $thumb instanceof MediaTransformError,
440 'Unknown MediaTransformOutput: ' . get_class( $thumb )
441 );
442 $label = $thumb->toText();
443 } else {
444 $label = $frameParams['alt'] ?? '';
445 }
447 $title, $label, '', '', '', (bool)$time, $handlerParams, $currentExists
448 );
449 } else {
450 self::processResponsiveImages( $file, $thumb, $handlerParams );
451 $params = [];
452 // An empty alt indicates an image is not a key part of the content
453 // and that non-visual browsers may omit it from rendering. Only
454 // set the parameter if it's explicitly requested.
455 if ( isset( $frameParams['alt'] ) ) {
456 $params['alt'] = $frameParams['alt'];
457 }
458 $params['title'] = $frameParams['title'];
459 $params += [
460 'img-class' => 'mw-file-element',
461 ];
462
463 if ( isset( $frameParams['upright'] ) ) {
464 $params['img-class'] .= ' mw-file-upright';
465 $params['style'] = '--mw-file-upright: ' . $frameParams['upright'];
466 }
467 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
468 $s = $thumb->toHtml( $params );
469 }
470
471 $wrapper = 'span';
472 $caption = '';
473
474 if ( $frameParams['align'] != '' ) {
475 $wrapper = 'figure';
476 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
477 $classes[] = "mw-halign-{$frameParams['align']}";
478 $caption = Html::rawElement(
479 'figcaption', [], $frameParams['caption'] ?? ''
480 );
481 } elseif ( isset( $frameParams['valign'] ) ) {
482 // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
483 // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
484 // mw-valign-text-bottom
485 $classes[] = "mw-valign-{$frameParams['valign']}";
486 }
487
488 if ( isset( $frameParams['border'] ) ) {
489 $classes[] = 'mw-image-border';
490 }
491
492 if ( isset( $frameParams['class'] ) ) {
493 $classes[] = $frameParams['class'];
494 }
495
496 $attribs = [
497 'class' => $classes,
498 'typeof' => $rdfaType,
499 ];
500
501 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
502
503 return str_replace( "\n", ' ', $s );
504 }
505
514 public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
515 $mtoParams = [];
516 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
517 $mtoParams['custom-url-link'] = $frameParams['link-url'];
518 if ( isset( $frameParams['link-target'] ) ) {
519 $mtoParams['custom-target-link'] = $frameParams['link-target'];
520 }
521 if ( $parser ) {
522 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
523 foreach ( $extLinkAttrs as $name => $val ) {
524 // Currently could include 'rel' and 'target'
525 $mtoParams['parser-extlink-' . $name] = $val;
526 }
527 }
528 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
529 $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
530 self::getLinkRenderer()->normalizeTarget( $frameParams['link-title'] )
531 );
532 if ( isset( $frameParams['link-title-query'] ) ) {
533 $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
534 }
535 } elseif ( !empty( $frameParams['no-link'] ) ) {
536 // No link
537 } else {
538 $mtoParams['desc-link'] = true;
539 $mtoParams['desc-query'] = $query;
540 }
541 return $mtoParams;
542 }
543
556 public static function makeThumbLinkObj(
557 LinkTarget $title, $file, $label = '', $alt = '', $align = null,
558 $params = [], $framed = false, $manualthumb = ''
559 ) {
560 $frameParams = [
561 'alt' => $alt,
562 'caption' => $label,
563 'align' => $align
564 ];
565 $classes = [];
566 if ( $manualthumb ) {
567 $frameParams['manualthumb'] = $manualthumb;
568 } elseif ( $framed ) {
569 $frameParams['framed'] = true;
570 } elseif ( !isset( $params['width'] ) ) {
571 $classes[] = 'mw-default-size';
572 }
574 $title, $file, $frameParams, $params, false, '', $classes
575 );
576 }
577
589 public static function makeThumbLink2(
590 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
591 $time = false, $query = '', array $classes = [], ?Parser $parser = null
592 ) {
593 $exists = $file && $file->exists();
594 $services = MediaWikiServices::getInstance();
595
596 $page = $handlerParams['page'] ?? false;
597 $lang = $handlerParams['lang'] ?? false;
598
599 if ( !isset( $frameParams['align'] ) ) {
600 $frameParams['align'] = '';
601 }
602 if ( !isset( $frameParams['caption'] ) ) {
603 $frameParams['caption'] = '';
604 }
605
606 if ( empty( $handlerParams['width'] ) ) {
607 // Reduce width for upright images when parameter 'upright' is used
608 $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
609 }
610
611 $thumb = false;
612 $noscale = false;
613 $manualthumb = false;
614 $manual_title = '';
615 $rdfaType = 'mw:File/Thumb';
616
617 if ( !$exists ) {
618 // Same precedence as the $exists case
619 if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
620 $rdfaType = 'mw:File/Frame';
621 }
622 $outerWidth = $handlerParams['width'] + 2;
623 } else {
624 if ( isset( $frameParams['manualthumb'] ) ) {
625 # Use manually specified thumbnail
626 $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
627 if ( $manual_title ) {
628 $manual_img = $services->getRepoGroup()
629 ->findFile( $manual_title );
630 if ( $manual_img ) {
631 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
632 $manualthumb = true;
633 }
634 }
635 } else {
636 $srcWidth = $file->getWidth( $page );
637 if ( isset( $frameParams['framed'] ) ) {
638 $rdfaType = 'mw:File/Frame';
639 if ( !$file->isVectorized() ) {
640 // Use image dimensions, don't scale
641 $noscale = true;
642 } else {
643 // framed is unscaled, but for vectorized images
644 // we need to a width for scaling up for the high density variants
645 $handlerParams['width'] = $srcWidth;
646 }
647 }
648
649 // Do not present an image bigger than the source, for bitmap-style images
650 // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
651 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
652 $handlerParams['width'] = $srcWidth;
653 }
654
655 $thumb = $noscale
656 ? $file->getUnscaledThumb( $handlerParams )
657 : $file->transform( $handlerParams );
658 }
659
660 if ( $thumb ) {
661 $outerWidth = $thumb->getWidth() + 2;
662 } else {
663 $outerWidth = $handlerParams['width'] + 2;
664 }
665 }
666
667 if ( $parser && $rdfaType === 'mw:File/Thumb' ) {
668 $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
669 }
670
671 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
672 $linkTitleQuery = [];
673 if ( $page || $lang ) {
674 if ( $page ) {
675 $linkTitleQuery['page'] = $page;
676 }
677 if ( $lang ) {
678 $linkTitleQuery['lang'] = $lang;
679 }
680 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
681 # So we don't need to pass it here in $query. However, the URL for the
682 # zoom icon still needs it, so we make a unique query for it. See T16771
683 $url = wfAppendQuery( $url, $linkTitleQuery );
684 }
685
686 if ( $manualthumb
687 && !isset( $frameParams['link-title'] )
688 && !isset( $frameParams['link-url'] )
689 && !isset( $frameParams['no-link'] ) ) {
690 $frameParams['link-title'] = $title;
691 $frameParams['link-title-query'] = $linkTitleQuery;
692 }
693
694 if ( $frameParams['align'] != '' ) {
695 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
696 $classes[] = "mw-halign-{$frameParams['align']}";
697 }
698
699 if ( isset( $frameParams['class'] ) ) {
700 $classes[] = $frameParams['class'];
701 }
702
703 $s = '';
704
705 $isBadFile = $exists && $thumb && $parser &&
706 $parser->getBadFileLookup()->isBadFile(
707 $manualthumb ? $manual_title->getDBkey() : $title->getDBkey(),
708 $parser->getTitle()
709 );
710
711 if ( !$exists ) {
712 $rdfaType = 'mw:Error ' . $rdfaType;
713 $label = $frameParams['alt'] ?? '';
715 $title, $label, '', '', '', (bool)$time, $handlerParams, false
716 );
717 $zoomIcon = '';
718 } elseif ( !$thumb || $thumb->isError() || $isBadFile ) {
719 $rdfaType = 'mw:Error ' . $rdfaType;
720 if ( $thumb && $thumb->isError() ) {
721 Assert::invariant(
722 $thumb instanceof MediaTransformError,
723 'Unknown MediaTransformOutput: ' . get_class( $thumb )
724 );
725 $label = $thumb->toText();
726 } elseif ( !$thumb ) {
727 $label = wfMessage( 'thumbnail_error', '' )->text();
728 } else {
729 $label = '';
730 }
732 $title, $label, '', '', '', (bool)$time, $handlerParams, true
733 );
734 $zoomIcon = '';
735 } else {
736 if ( !$noscale && !$manualthumb ) {
737 self::processResponsiveImages( $file, $thumb, $handlerParams );
738 }
739 $params = [];
740 // An empty alt indicates an image is not a key part of the content
741 // and that non-visual browsers may omit it from rendering. Only
742 // set the parameter if it's explicitly requested.
743 if ( isset( $frameParams['alt'] ) ) {
744 $params['alt'] = $frameParams['alt'];
745 }
746 $params += [
747 'img-class' => 'mw-file-element',
748 ];
749 if ( isset( $frameParams['upright'] ) ) {
750 $params['img-class'] .= ' mw-file-upright';
751 $params['style'] = '--mw-file-upright: ' . $frameParams['upright'];
752 }
753 // Only thumbs gets the magnify link
754 if ( $rdfaType === 'mw:File/Thumb' ) {
755 $params['magnify-resource'] = $url;
756 }
757 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
758 $s .= $thumb->toHtml( $params );
759 if ( isset( $frameParams['framed'] ) ) {
760 $zoomIcon = '';
761 } else {
762 $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
763 Html::rawElement( 'a', [
764 'href' => $url,
765 'class' => 'internal',
766 'title' => wfMessage( 'thumbnail-more' )->text(),
767 ] )
768 );
769 }
770 }
771
772 $s .= Html::rawElement(
773 'figcaption', [], $frameParams['caption'] ?? ''
774 );
775
776 $attribs = [
777 'class' => $classes,
778 'typeof' => $rdfaType,
779 ];
780
781 $s = Html::rawElement( 'figure', $attribs, $s );
782
783 return str_replace( "\n", ' ', $s );
784 }
785
794 public static function processResponsiveImages( $file, $thumb, $hp ) {
795 $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
796 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
797 $hp20 = $hp;
798 $hp20['width'] = $hp['width'] * 2;
799 if ( isset( $hp['height'] ) ) {
800 $hp20['height'] = $hp['height'] * 2;
801 }
802 $thumb20 = $file->transform( $hp20 );
803 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
804 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
805 }
806 }
807 }
808
823 public static function makeBrokenImageLinkObj(
824 $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
825 $time = false, array $handlerParams = [], bool $currentExists = false
826 ) {
827 if ( !$title instanceof LinkTarget ) {
828 wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
829 return "<!-- ERROR -->" . htmlspecialchars( $label );
830 }
831
832 $title = Title::newFromLinkTarget( $title );
833 $services = MediaWikiServices::getInstance();
834 $mainConfig = $services->getMainConfig();
835 $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
836 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
837 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
838 if ( $label == '' ) {
839 $label = $title->getPrefixedText();
840 }
841
842 $html = Html::element( 'span', [
843 'class' => 'mw-file-element mw-broken-media',
844 // These data attributes are used to dynamically size the span, see T273013
845 'data-width' => $handlerParams['width'] ?? null,
846 'data-height' => $handlerParams['height'] ?? null,
847 ], $label );
848
849 $repoGroup = $services->getRepoGroup();
850 $currentExists = $currentExists ||
851 ( $time && $repoGroup->findFile( $title ) !== false );
852
853 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
854 && !$currentExists
855 ) {
856 if (
857 $title->inNamespace( NS_FILE ) &&
858 $repoGroup->getLocalRepo()->checkRedirect( $title )
859 ) {
860 // We already know it's a redirect, so mark it accordingly
861 return self::link(
862 $title,
863 $html,
864 [ 'class' => 'mw-redirect' ],
865 wfCgiToArray( $query ),
866 [ 'known', 'noclasses' ]
867 );
868 }
869 return Html::rawElement( 'a', [
870 'href' => self::getUploadUrl( $title, $query ),
871 'class' => 'new',
872 'title' => $title->getPrefixedText()
873 ], $html );
874 }
875 return self::link(
876 $title,
877 $html,
878 [],
879 wfCgiToArray( $query ),
880 [ 'known', 'noclasses' ]
881 );
882 }
883
892 public static function getUploadUrl( $destFile, $query = '' ) {
893 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
894 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
895 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
896 $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
897 if ( $query != '' ) {
898 $q .= '&' . $query;
899 }
900
901 if ( $uploadMissingFileUrl ) {
902 return wfAppendQuery( $uploadMissingFileUrl, $q );
903 }
904
905 if ( $uploadNavigationUrl ) {
906 return wfAppendQuery( $uploadNavigationUrl, $q );
907 }
908
909 $upload = SpecialPage::getTitleFor( 'Upload' );
910
911 return $upload->getLocalURL( $q );
912 }
913
923 public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
924 $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
925 $title, [ 'time' => $time ]
926 );
927 return self::makeMediaLinkFile( $title, $img, $html );
928 }
929
942 public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
943 if ( $file && $file->exists() ) {
944 $url = $file->getUrl();
945 $class = 'internal';
946 } else {
947 $url = self::getUploadUrl( $title );
948 $class = 'new';
949 }
950
951 $alt = $title->getText();
952 if ( $html == '' ) {
953 $html = $alt;
954 }
955
956 $ret = '';
957 $attribs = [
958 'href' => $url,
959 'class' => $class,
960 'title' => $alt
961 ];
962
963 if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
964 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
965 ) {
966 wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
967 . "with url {$url} and text {$html} to {$ret}" );
968 return $ret;
969 }
970
971 return Html::rawElement( 'a', $attribs, $html );
972 }
973
984 public static function specialLink( $name, $key = '' ) {
985 $queryPos = strpos( $name, '?' );
986 if ( $queryPos !== false ) {
987 $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
988 $name = substr( $name, 0, $queryPos );
989 } else {
990 $getParams = [];
991 }
992
993 $slashPos = strpos( $name, '/' );
994 if ( $slashPos !== false ) {
995 $subpage = substr( $name, $slashPos + 1 );
996 $name = substr( $name, 0, $slashPos );
997 } else {
998 $subpage = false;
999 }
1000
1001 if ( $key == '' ) {
1002 $key = strtolower( $name );
1003 }
1004
1005 return self::linkKnown(
1006 SpecialPage::getTitleFor( $name, $subpage ),
1007 wfMessage( $key )->escaped(),
1008 [],
1009 $getParams
1010 );
1011 }
1012
1033 public static function makeExternalLink( $url, $text, $escape = true,
1034 $linktype = '', $attribs = [], $title = null
1035 ) {
1036 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1037 global $wgTitle;
1038 return self::getLinkRenderer()->makeExternalLink(
1039 $url,
1040 $escape ? $text : new HtmlArmor( $text ),
1041 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1042 $linktype,
1043 $attribs
1044 );
1045 }
1046
1061 public static function userLink(
1062 $userId,
1063 $userName,
1064 $altUserName = false,
1065 $attributes = []
1066 ) {
1067 if ( $userName === '' || $userName === false || $userName === null ) {
1068 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1069 'that need to be fixed?' );
1070 return wfMessage( 'empty-username' )->parse();
1071 }
1072
1073 return self::getLinkRenderer()->makeUserLink(
1074 new UserIdentityValue( $userId, (string)$userName ),
1075 RequestContext::getMain(),
1076 $altUserName === false ? null : (string)$altUserName,
1077 $attributes
1078 );
1079 }
1080
1099 public static function userToolLinkArray(
1100 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1101 ): array {
1102 $services = MediaWikiServices::getInstance();
1103 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1104 $talkable = !( $disableAnonTalk && $userId == 0 );
1105 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1106 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1107
1108 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1109 // No tools for an external user
1110 return [];
1111 }
1112
1113 $items = [];
1114 if ( $talkable ) {
1115 $items[] = self::userTalkLink( $userId, $userText );
1116 }
1117
1118 // (T412013) Do not link to Special:Contributions for temp accounts
1119 // since the target for the link in the username itself already links to
1120 // Special:Contributions.
1121 if ( $userId && !$services->getTempUserConfig()->isTempName( $userText ) ) {
1122 $attribs = [];
1123 $attribs['class'] = 'mw-usertoollinks-contribs';
1124
1125 // check if the user has edits
1126 if ( $redContribsWhenNoEdits ) {
1127 if ( $edits === null ) {
1128 $user = UserIdentityValue::newRegistered( $userId, $userText );
1129 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1130 }
1131 if ( $edits === 0 ) {
1132 // Note: "new" class is inappropriate here, as "new" class
1133 // should only be used for pages that do not exist.
1134 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1135 }
1136 }
1137 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1138
1139 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1140 }
1141 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1142 if ( $blockable && $userCanBlock ) {
1143 $items[] = self::blockLink( $userId, $userText );
1144 }
1145
1146 if (
1147 $addEmailLink
1148 && MediaWikiServices::getInstance()->getEmailUserFactory()
1149 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1150 ->canSend()
1151 ->isGood()
1152 ) {
1153 $items[] = self::emailLink( $userId, $userText );
1154 }
1155
1156 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1157
1158 return $items;
1159 }
1160
1168 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1169 if ( !$items ) {
1170 return '';
1171 }
1172
1173 if ( $useParentheses ) {
1174 $lang = RequestContext::getMain()->getLanguage();
1175 return wfMessage( 'word-separator' )->escaped()
1176 . '<span class="mw-usertoollinks">'
1177 . wfMessage( 'parentheses' )->rawParams( $lang->pipeList( $items ) )->escaped()
1178 . '</span>';
1179 }
1180
1181 $tools = [];
1182 foreach ( $items as $tool ) {
1183 $tools[] = Html::rawElement( 'span', [], $tool );
1184 }
1185 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1186 implode( ' ', $tools ) . '</span>';
1187 }
1188
1203 public static function userToolLinks(
1204 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1205 $useParentheses = true
1206 ) {
1207 if ( $userText === '' ) {
1208 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1209 'that need to be fixed?' );
1210 return ' ' . wfMessage( 'empty-username' )->parse();
1211 }
1212
1213 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1214 return self::renderUserToolLinksArray( $items, $useParentheses );
1215 }
1216
1226 public static function userToolLinksRedContribs(
1227 $userId, $userText, $edits = null, $useParentheses = true
1228 ) {
1229 return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1230 }
1231
1238 public static function userTalkLink( $userId, $userText ) {
1239 if ( $userText === '' ) {
1240 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1241 'that need to be fixed?' );
1242 return wfMessage( 'empty-username' )->parse();
1243 }
1244
1245 $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1246 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1247 $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1248
1249 return $userTalkPage
1250 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1251 : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1252 }
1253
1260 public static function blockLink( $userId, $userText ) {
1261 if ( $userText === '' ) {
1262 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1263 'that need to be fixed?' );
1264 return wfMessage( 'empty-username' )->parse();
1265 }
1266
1267 $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1268 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1269
1270 return self::link( $blockPage,
1271 wfMessage( 'blocklink' )->escaped(),
1272 $moreLinkAttribs
1273 );
1274 }
1275
1281 public static function emailLink( $userId, $userText ) {
1282 if ( $userText === '' ) {
1283 wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1284 'that need to be fixed?' );
1285 return wfMessage( 'empty-username' )->parse();
1286 }
1287
1288 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1289 $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1290 return self::link( $emailPage,
1291 wfMessage( 'emaillink' )->escaped(),
1292 $moreLinkAttribs
1293 );
1294 }
1295
1307 public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1308 // TODO inject authority
1309 $authority = RequestContext::getMain()->getAuthority();
1310
1311 $revUser = $revRecord->getUser(
1312 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1313 $authority
1314 );
1315 if ( $revUser ) {
1316 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1317 } else {
1318 // User is deleted and we can't (or don't want to) view it
1319 $link = wfMessage( 'rev-deleted-user' )->escaped();
1320 }
1321
1322 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1323 $class = self::getRevisionDeletedClass( $revRecord );
1324 return '<span class="' . $class . '">' . $link . '</span>';
1325 }
1326 return $link;
1327 }
1328
1335 public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1336 $class = 'history-deleted';
1337 if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1338 $class .= ' mw-history-suppressed';
1339 }
1340 return $class;
1341 }
1342
1355 public static function revUserTools(
1356 RevisionRecord $revRecord,
1357 $isPublic = false,
1358 $useParentheses = true
1359 ) {
1360 // TODO inject authority
1361 $authority = RequestContext::getMain()->getAuthority();
1362
1363 $revUser = $revRecord->getUser(
1364 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1365 $authority
1366 );
1367 if ( $revUser ) {
1368 $link = self::userLink(
1369 $revUser->getId(),
1370 $revUser->getName(),
1371 false,
1372 [ 'data-mw-revid' => $revRecord->getId() ]
1373 ) . self::userToolLinks(
1374 $revUser->getId(),
1375 $revUser->getName(),
1376 false,
1377 0,
1378 null,
1379 $useParentheses
1380 );
1381 } else {
1382 // User is deleted and we can't (or don't want to) view it
1383 $link = wfMessage( 'rev-deleted-user' )->escaped();
1384 }
1385
1386 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1387 $class = self::getRevisionDeletedClass( $revRecord );
1388 return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1389 }
1390 return $link;
1391 }
1392
1403 public static function expandLocalLinks( string $html ) {
1404 return HtmlHelper::modifyElements(
1405 $html,
1406 static function ( SerializerNode $node ): bool {
1407 return $node->name === 'a' && isset( $node->attrs['href'] );
1408 },
1409 static function ( SerializerNode $node ): SerializerNode {
1410 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1411 $href = $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE );
1412 if ( $href !== null ) {
1413 $node->attrs['href'] = $href;
1414 }
1415 return $node;
1416 }
1417 );
1418 }
1419
1426 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1427 # Valid link forms:
1428 # Foobar -- normal
1429 # :Foobar -- override special treatment of prefix (images, language links)
1430 # /Foobar -- convert to CurrentPage/Foobar
1431 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1432 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1433 # ../Foobar -- convert to CurrentPage/Foobar,
1434 # (from CurrentPage/CurrentSubPage)
1435 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1436 # (from CurrentPage/CurrentSubPage)
1437
1438 $ret = $target; # default return value is no change
1439
1440 # Some namespaces don't allow subpages,
1441 # so only perform processing if subpages are allowed
1442 if (
1443 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1444 hasSubpages( $contextTitle->getNamespace() )
1445 ) {
1446 $hash = strpos( $target, '#' );
1447 if ( $hash !== false ) {
1448 $suffix = substr( $target, $hash );
1449 $target = substr( $target, 0, $hash );
1450 } else {
1451 $suffix = '';
1452 }
1453 # T9425
1454 $target = trim( $target );
1455 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1456 getPrefixedText( $contextTitle );
1457 # Look at the first character
1458 if ( $target != '' && $target[0] === '/' ) {
1459 # / at end means we don't want the slash to be shown
1460 $m = [];
1461 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1462 if ( $trailingSlashes ) {
1463 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1464 } else {
1465 $noslash = substr( $target, 1 );
1466 }
1467
1468 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1469 if ( $text === '' ) {
1470 $text = $target . $suffix;
1471 } # this might be changed for ugliness reasons
1472 } else {
1473 # check for .. subpage backlinks
1474 $dotdotcount = 0;
1475 $nodotdot = $target;
1476 while ( str_starts_with( $nodotdot, '../' ) ) {
1477 ++$dotdotcount;
1478 $nodotdot = substr( $nodotdot, 3 );
1479 }
1480 if ( $dotdotcount > 0 ) {
1481 $exploded = explode( '/', $contextPrefixedText );
1482 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1483 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1484 # / at the end means don't show full path
1485 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1486 $nodotdot = rtrim( $nodotdot, '/' );
1487 if ( $text === '' ) {
1488 $text = $nodotdot . $suffix;
1489 }
1490 }
1491 $nodotdot = trim( $nodotdot );
1492 if ( $nodotdot != '' ) {
1493 $ret .= '/' . $nodotdot;
1494 }
1495 $ret .= $suffix;
1496 }
1497 }
1498 }
1499 }
1500
1501 return $ret;
1502 }
1503
1509 public static function formatRevisionSize( $size ) {
1510 if ( $size == 0 ) {
1511 $stxt = wfMessage( 'historyempty' )->escaped();
1512 } else {
1513 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1514 }
1515 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1516 }
1517
1524 public static function splitTrail( $trail ) {
1525 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1526 $inside = '';
1527 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1528 [ , $inside, $trail ] = $m;
1529 }
1530 return [ $inside, $trail ];
1531 }
1532
1563 public static function generateRollback(
1564 RevisionRecord $revRecord,
1565 ?IContextSource $context = null,
1566 $options = []
1567 ) {
1568 $context ??= RequestContext::getMain();
1569
1570 $editCount = self::getRollbackEditCount( $revRecord );
1571 if ( $editCount === false ) {
1572 return '';
1573 }
1574
1575 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1576
1577 $services = MediaWikiServices::getInstance();
1578 // Allow extensions to modify the rollback link.
1579 // Abort further execution if the extension wants full control over the link.
1580 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1581 $revRecord, $context, $options, $inner ) ) {
1582 return $inner;
1583 }
1584
1585 if ( !in_array( 'noBrackets', $options, true ) ) {
1586 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1587 }
1588
1589 if ( $services->getUserOptionsLookup()
1590 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1591 ) {
1592 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1593 }
1594
1595 return '<span class="mw-rollback-link">' . $inner . '</span>';
1596 }
1597
1616 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1617 if ( func_num_args() > 1 ) {
1618 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1619 }
1620 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1621 ->get( MainConfigNames::ShowRollbackEditCount );
1622
1623 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1624 // Nothing has happened, indicate this by returning 'null'
1625 return null;
1626 }
1627
1628 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1629
1630 // Up to the value of $wgShowRollbackEditCount revisions are counted
1631 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1632 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1633 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1634 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1635 ->limit( $showRollbackEditCount + 1 )
1636 ->caller( __METHOD__ )->fetchResultSet();
1637
1638 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1639 $revUserText = $revUser ? $revUser->getName() : '';
1640
1641 $editCount = 0;
1642 $moreRevs = false;
1643 foreach ( $res as $row ) {
1644 if ( $row->rev_user_text != $revUserText ) {
1645 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1646 || $row->rev_deleted & RevisionRecord::DELETED_USER
1647 ) {
1648 // If the user or the text of the revision we might rollback
1649 // to is deleted in some way we can't rollback. Similar to
1650 // the checks in WikiPage::commitRollback.
1651 return false;
1652 }
1653 $moreRevs = true;
1654 break;
1655 }
1656 $editCount++;
1657 }
1658
1659 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1660 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1661 // and there weren't any other revisions. That means that the current user is the only
1662 // editor, so we can't rollback
1663 return false;
1664 }
1665 return $editCount;
1666 }
1667
1682 public static function buildRollbackLink(
1683 RevisionRecord $revRecord,
1684 ?IContextSource $context = null,
1685 $editCount = false
1686 ) {
1687 $config = MediaWikiServices::getInstance()->getMainConfig();
1688 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1689 $miserMode = $config->get( MainConfigNames::MiserMode );
1690 // To config which pages are affected by miser mode
1691 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1692
1693 $context ??= RequestContext::getMain();
1694
1695 $title = $revRecord->getPageAsLinkTarget();
1696 $revUser = $revRecord->getUser();
1697 $revUserText = $revUser ? $revUser->getName() : '';
1698
1699 $query = [
1700 'action' => 'rollback',
1701 'from' => $revUserText,
1702 'token' => $context->getUser()->getEditToken( 'rollback' ),
1703 ];
1704
1705 $attrs = [
1706 'data-mw-interface' => '',
1707 'title' => $context->msg( 'tooltip-rollback' )->text()
1708 ];
1709
1710 $options = [ 'known', 'noclasses' ];
1711
1712 if ( $context->getRequest()->getBool( 'bot' ) ) {
1713 // T17999
1714 $query['hidediff'] = '1';
1715 $query['bot'] = '1';
1716 }
1717
1718 if ( $miserMode ) {
1719 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1720 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1721 $showRollbackEditCount = false;
1722 break;
1723 }
1724 }
1725 }
1726
1727 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1728 $msg = [ 'rollbacklink' ];
1729 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1730 if ( !is_numeric( $editCount ) ) {
1731 $editCount = self::getRollbackEditCount( $revRecord );
1732 }
1733
1734 if ( $editCount > $showRollbackEditCount ) {
1735 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1736 } elseif ( $editCount ) {
1737 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1738 }
1739 }
1740
1741 $html = $context->msg( ...$msg )->parse();
1742 return self::link( $title, $html, $attrs, $query, $options );
1743 }
1744
1753 public static function formatHiddenCategories( $hiddencats ) {
1754 $outText = '';
1755 if ( count( $hiddencats ) > 0 ) {
1756 # Construct the HTML
1757 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1758 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1759 $outText .= "</div><ul>\n";
1760
1761 foreach ( $hiddencats as $titleObj ) {
1762 # If it's hidden, it must exist - no need to check with a LinkBatch
1763 $outText .= '<li>'
1764 . self::link( $titleObj, null, [], [], 'known' )
1765 . "</li>\n";
1766 }
1767 $outText .= '</ul>';
1768 }
1769 return $outText;
1770 }
1771
1775 private static function getContextFromMain() {
1776 $context = RequestContext::getMain();
1777 $context = new DerivativeContext( $context );
1778 return $context;
1779 }
1780
1798 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1799 if ( !$localizer ) {
1800 $localizer = self::getContextFromMain();
1801 }
1802 $message = $localizer->msg( "tooltip-$name", $msgParams );
1803 // Set a default tooltip for subject namespace tabs if that hasn't
1804 // been defined. See T22126
1805 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1806 $message = $localizer->msg( 'tooltip-ca-nstab' );
1807 }
1808
1809 if ( $message->isDisabled() ) {
1810 $tooltip = false;
1811 } else {
1812 $tooltip = $message->text();
1813 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1814 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1815 }
1816
1817 $options = (array)$options;
1818
1819 if ( in_array( 'nonexisting', $options ) ) {
1820 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1821 }
1822 if ( in_array( 'withaccess', $options ) ) {
1823 $accesskey = self::accesskey( $name, $localizer );
1824 if ( $accesskey !== false ) {
1825 // Should be build the same as in jquery.accessKeyLabel.js
1826 if ( $tooltip === false || $tooltip === '' ) {
1827 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1828 } else {
1829 $tooltip .= $localizer->msg( 'word-separator' )->text();
1830 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1831 }
1832 }
1833 }
1834
1835 return $tooltip;
1836 }
1837
1839 public static $accesskeycache;
1840
1853 public static function accesskey( $name, $localizer = null ) {
1854 if ( !isset( self::$accesskeycache[$name] ) ) {
1855 if ( !$localizer ) {
1856 $localizer = self::getContextFromMain();
1857 }
1858 $msg = $localizer->msg( "accesskey-$name" );
1859 // Set a default accesskey for subject namespace tabs if an
1860 // accesskey has not been defined. See T22126
1861 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1862 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1863 }
1864 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1865 }
1866 return self::$accesskeycache[$name];
1867 }
1868
1883 public static function getRevDeleteLink(
1884 Authority $performer,
1885 RevisionRecord $revRecord,
1886 LinkTarget $title
1887 ) {
1888 $canHide = $performer->isAllowed( 'deleterevision' );
1889 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
1890 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
1891 return '';
1892 }
1893
1894 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
1895 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
1896 }
1897 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
1898 getPrefixedDBkey( $title );
1899 if ( $revRecord->getId() ) {
1900 // RevDelete links using revision ID are stable across
1901 // page deletion and undeletion; use when possible.
1902 $query = [
1903 'type' => 'revision',
1904 'target' => $prefixedDbKey,
1905 'ids' => $revRecord->getId()
1906 ];
1907 } else {
1908 // Older deleted entries didn't save a revision ID.
1909 // We have to refer to these by timestamp, ick!
1910 $query = [
1911 'type' => 'archive',
1912 'target' => $prefixedDbKey,
1913 'ids' => $revRecord->getTimestamp()
1914 ];
1915 }
1916 return self::revDeleteLink(
1917 $query,
1918 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
1919 $canHide
1920 );
1921 }
1922
1935 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
1936 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
1937 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1938 $html = wfMessage( $msgKey )->escaped();
1939 $tag = $restricted ? 'strong' : 'span';
1940 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
1941 return Html::rawElement(
1942 $tag,
1943 [ 'class' => 'mw-revdelundel-link' ],
1944 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
1945 );
1946 }
1947
1959 public static function revDeleteLinkDisabled( $delete = true ) {
1960 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1961 $html = wfMessage( $msgKey )->escaped();
1962 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
1963 return Html::rawElement( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
1964 }
1965
1979 public static function tooltipAndAccesskeyAttribs(
1980 $name,
1981 array $msgParams = [],
1982 $options = null,
1983 $localizer = null
1984 ) {
1985 $options = (array)$options;
1986 $options[] = 'withaccess';
1987
1988 // Get optional parameters from global context if any missing.
1989 if ( !$localizer ) {
1990 $localizer = self::getContextFromMain();
1991 }
1992
1993 $attribs = [
1994 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
1995 'accesskey' => self::accesskey( $name, $localizer )
1996 ];
1997 if ( $attribs['title'] === false ) {
1998 unset( $attribs['title'] );
1999 }
2000 if ( $attribs['accesskey'] === false ) {
2001 unset( $attribs['accesskey'] );
2002 }
2003 return $attribs;
2004 }
2005
2013 public static function tooltip( $name, $options = null ) {
2014 $tooltip = self::titleAttrib( $name, $options );
2015 if ( $tooltip === false ) {
2016 return '';
2017 }
2018 return Html::expandAttributes( [
2019 'title' => $tooltip
2020 ] );
2021 }
2022
2027 private static function getLinkRenderer(
2028 array $legacyOptions = []
2029 ): LinkRenderer {
2030 $services = MediaWikiServices::getInstance();
2031
2032 if ( count( $legacyOptions ) > 0 ) {
2033 return $services->getLinkRendererFactory()->createFromLegacyOptions(
2034 $legacyOptions
2035 );
2036 }
2037
2038 return $services->getLinkRenderer();
2039 }
2040
2041}
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const PROTO_RELATIVE
Definition Defines.php:219
const NS_USER_TALK
Definition Defines.php:54
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(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:549
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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:80
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:44
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:47
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition Linker.php:1403
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition Linker.php:1935
static link( $target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition Linker.php:95
static string false[] $accesskeycache
Definition Linker.php:1839
static blockLink( $userId, $userText)
Definition Linker.php:1260
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='', $hash='')
Make appropriate markup for a link to the current article.
Definition Linker.php:170
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null, $localizer=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:1979
static getUploadUrl( $destFile, $query='')
Get the URL to upload a certain file.
Definition Linker.php:892
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:304
static makeMediaLinkObj( $title, $html='', $time=false)
Create a direct link to a given uploaded file.
Definition Linker.php:923
static processResponsiveImages( $file, $thumb, $hp)
Add 2x variant for srcset, if $wgResponsiveImages is enabled.
Definition Linker.php:794
static userTalkLink( $userId, $userText)
Definition Linker.php:1238
static generateRollback(RevisionRecord $revRecord, ?IContextSource $context=null, $options=[])
Generate a rollback link for a given revision.
Definition Linker.php:1563
static buildRollbackLink(RevisionRecord $revRecord, ?IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1682
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1426
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:984
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:1203
static emailLink( $userId, $userText)
Definition Linker.php:1281
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1753
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition Linker.php:203
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:1616
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1033
static userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1099
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition Linker.php:514
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition Linker.php:146
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:1883
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query='', array $classes=[], ?Parser $parser=null)
Definition Linker.php:589
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:246
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition Linker.php:2013
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[], bool $currentExists=false)
Make a "broken" link to an image.
Definition Linker.php:823
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:942
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1853
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:1798
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1168
static userToolLinksRedContribs( $userId, $userText, $edits=null, $useParentheses=true)
Alias for userToolLinks( $userId, $userText, true );.
Definition Linker.php:1226
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:1524
const TOOL_LINKS_NOBLOCK
Flags for userToolLinks()
Definition Linker.php:51
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition Linker.php:1959
static formatRevisionSize( $size)
Definition Linker.php:1509
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:556
static userLink( $userId, $userName, $altUserName=false, $attributes=[])
Make user link (or user contributions for unregistered users)
Definition Linker.php:1061
static revUserLink(RevisionRecord $revRecord, $isPublic=false)
Generate a user link if the current user is allowed to view it.
Definition Linker.php:1307
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1335
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:1355
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.
Basic media transform error class.
Base class for the output of MediaHandler::doTransform() and File::transform().
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:139
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1154
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 given 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:69
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:18
Build SELECT queries with a fluent interface.
Interface for objects which can provide a MediaWiki context on request.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
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:23
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
element(SerializerNode $parent, SerializerNode $node, $contents)