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