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 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
463 $s = $thumb->toHtml( $params );
464 }
465
466 $wrapper = 'span';
467 $caption = '';
468
469 if ( $frameParams['align'] != '' ) {
470 $wrapper = 'figure';
471 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
472 $classes[] = "mw-halign-{$frameParams['align']}";
473 $caption = Html::rawElement(
474 'figcaption', [], $frameParams['caption'] ?? ''
475 );
476 } elseif ( isset( $frameParams['valign'] ) ) {
477 // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
478 // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
479 // mw-valign-text-bottom
480 $classes[] = "mw-valign-{$frameParams['valign']}";
481 }
482
483 if ( isset( $frameParams['border'] ) ) {
484 $classes[] = 'mw-image-border';
485 }
486
487 if ( isset( $frameParams['class'] ) ) {
488 $classes[] = $frameParams['class'];
489 }
490
491 $attribs = [
492 'class' => $classes,
493 'typeof' => $rdfaType,
494 ];
495
496 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
497
498 return str_replace( "\n", ' ', $s );
499 }
500
509 public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
510 $mtoParams = [];
511 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
512 $mtoParams['custom-url-link'] = $frameParams['link-url'];
513 if ( isset( $frameParams['link-target'] ) ) {
514 $mtoParams['custom-target-link'] = $frameParams['link-target'];
515 }
516 if ( $parser ) {
517 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
518 foreach ( $extLinkAttrs as $name => $val ) {
519 // Currently could include 'rel' and 'target'
520 $mtoParams['parser-extlink-' . $name] = $val;
521 }
522 }
523 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
524 $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
525 self::getLinkRenderer()->normalizeTarget( $frameParams['link-title'] )
526 );
527 if ( isset( $frameParams['link-title-query'] ) ) {
528 $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
529 }
530 } elseif ( !empty( $frameParams['no-link'] ) ) {
531 // No link
532 } else {
533 $mtoParams['desc-link'] = true;
534 $mtoParams['desc-query'] = $query;
535 }
536 return $mtoParams;
537 }
538
551 public static function makeThumbLinkObj(
552 LinkTarget $title, $file, $label = '', $alt = '', $align = null,
553 $params = [], $framed = false, $manualthumb = ''
554 ) {
555 $frameParams = [
556 'alt' => $alt,
557 'caption' => $label,
558 'align' => $align
559 ];
560 $classes = [];
561 if ( $manualthumb ) {
562 $frameParams['manualthumb'] = $manualthumb;
563 } elseif ( $framed ) {
564 $frameParams['framed'] = true;
565 } elseif ( !isset( $params['width'] ) ) {
566 $classes[] = 'mw-default-size';
567 }
569 $title, $file, $frameParams, $params, false, '', $classes
570 );
571 }
572
584 public static function makeThumbLink2(
585 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
586 $time = false, $query = '', array $classes = [], ?Parser $parser = null
587 ) {
588 $exists = $file && $file->exists();
589 $services = MediaWikiServices::getInstance();
590
591 $page = $handlerParams['page'] ?? false;
592 $lang = $handlerParams['lang'] ?? false;
593
594 if ( !isset( $frameParams['align'] ) ) {
595 $frameParams['align'] = '';
596 }
597 if ( !isset( $frameParams['caption'] ) ) {
598 $frameParams['caption'] = '';
599 }
600
601 if ( empty( $handlerParams['width'] ) ) {
602 // Reduce width for upright images when parameter 'upright' is used
603 $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
604 }
605
606 $thumb = false;
607 $noscale = false;
608 $manualthumb = false;
609 $manual_title = '';
610 $rdfaType = 'mw:File/Thumb';
611
612 if ( !$exists ) {
613 // Same precedence as the $exists case
614 if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
615 $rdfaType = 'mw:File/Frame';
616 }
617 $outerWidth = $handlerParams['width'] + 2;
618 } else {
619 if ( isset( $frameParams['manualthumb'] ) ) {
620 # Use manually specified thumbnail
621 $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
622 if ( $manual_title ) {
623 $manual_img = $services->getRepoGroup()
624 ->findFile( $manual_title );
625 if ( $manual_img ) {
626 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
627 $manualthumb = true;
628 }
629 }
630 } else {
631 $srcWidth = $file->getWidth( $page );
632 if ( isset( $frameParams['framed'] ) ) {
633 $rdfaType = 'mw:File/Frame';
634 if ( !$file->isVectorized() ) {
635 // Use image dimensions, don't scale
636 $noscale = true;
637 } else {
638 // framed is unscaled, but for vectorized images
639 // we need to a width for scaling up for the high density variants
640 $handlerParams['width'] = $srcWidth;
641 }
642 }
643
644 // Do not present an image bigger than the source, for bitmap-style images
645 // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
646 if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
647 $handlerParams['width'] = $srcWidth;
648 }
649
650 $thumb = $noscale
651 ? $file->getUnscaledThumb( $handlerParams )
652 : $file->transform( $handlerParams );
653 }
654
655 if ( $thumb ) {
656 $outerWidth = $thumb->getWidth() + 2;
657 } else {
658 $outerWidth = $handlerParams['width'] + 2;
659 }
660 }
661
662 if ( $parser && $rdfaType === 'mw:File/Thumb' ) {
663 $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
664 }
665
666 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
667 $linkTitleQuery = [];
668 if ( $page || $lang ) {
669 if ( $page ) {
670 $linkTitleQuery['page'] = $page;
671 }
672 if ( $lang ) {
673 $linkTitleQuery['lang'] = $lang;
674 }
675 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
676 # So we don't need to pass it here in $query. However, the URL for the
677 # zoom icon still needs it, so we make a unique query for it. See T16771
678 $url = wfAppendQuery( $url, $linkTitleQuery );
679 }
680
681 if ( $manualthumb
682 && !isset( $frameParams['link-title'] )
683 && !isset( $frameParams['link-url'] )
684 && !isset( $frameParams['no-link'] ) ) {
685 $frameParams['link-title'] = $title;
686 $frameParams['link-title-query'] = $linkTitleQuery;
687 }
688
689 if ( $frameParams['align'] != '' ) {
690 // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
691 $classes[] = "mw-halign-{$frameParams['align']}";
692 }
693
694 if ( isset( $frameParams['class'] ) ) {
695 $classes[] = $frameParams['class'];
696 }
697
698 $s = '';
699
700 $isBadFile = $exists && $thumb && $parser &&
701 $parser->getBadFileLookup()->isBadFile(
702 $manualthumb ? $manual_title->getDBkey() : $title->getDBkey(),
703 $parser->getTitle()
704 );
705
706 if ( !$exists ) {
707 $rdfaType = 'mw:Error ' . $rdfaType;
708 $label = $frameParams['alt'] ?? '';
710 $title, $label, '', '', '', (bool)$time, $handlerParams, false
711 );
712 $zoomIcon = '';
713 } elseif ( !$thumb || $thumb->isError() || $isBadFile ) {
714 $rdfaType = 'mw:Error ' . $rdfaType;
715 if ( $thumb && $thumb->isError() ) {
716 Assert::invariant(
717 $thumb instanceof MediaTransformError,
718 'Unknown MediaTransformOutput: ' . get_class( $thumb )
719 );
720 $label = $thumb->toText();
721 } elseif ( !$thumb ) {
722 $label = wfMessage( 'thumbnail_error', '' )->text();
723 } else {
724 $label = '';
725 }
727 $title, $label, '', '', '', (bool)$time, $handlerParams, true
728 );
729 $zoomIcon = '';
730 } else {
731 if ( !$noscale && !$manualthumb ) {
732 self::processResponsiveImages( $file, $thumb, $handlerParams );
733 }
734 $params = [];
735 // An empty alt indicates an image is not a key part of the content
736 // and that non-visual browsers may omit it from rendering. Only
737 // set the parameter if it's explicitly requested.
738 if ( isset( $frameParams['alt'] ) ) {
739 $params['alt'] = $frameParams['alt'];
740 }
741 $params += [
742 'img-class' => 'mw-file-element',
743 ];
744 // Only thumbs gets the magnify link
745 if ( $rdfaType === 'mw:File/Thumb' ) {
746 $params['magnify-resource'] = $url;
747 }
748 $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
749 $s .= $thumb->toHtml( $params );
750 if ( isset( $frameParams['framed'] ) ) {
751 $zoomIcon = '';
752 } else {
753 $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
754 Html::rawElement( 'a', [
755 'href' => $url,
756 'class' => 'internal',
757 'title' => wfMessage( 'thumbnail-more' )->text(),
758 ] )
759 );
760 }
761 }
762
763 $s .= Html::rawElement(
764 'figcaption', [], $frameParams['caption'] ?? ''
765 );
766
767 $attribs = [
768 'class' => $classes,
769 'typeof' => $rdfaType,
770 ];
771
772 $s = Html::rawElement( 'figure', $attribs, $s );
773
774 return str_replace( "\n", ' ', $s );
775 }
776
785 public static function processResponsiveImages( $file, $thumb, $hp ) {
786 $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
787 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
788 $hp15 = $hp;
789 $hp15['width'] = round( $hp['width'] * 1.5 );
790 $hp20 = $hp;
791 $hp20['width'] = $hp['width'] * 2;
792 if ( isset( $hp['height'] ) ) {
793 $hp15['height'] = round( $hp['height'] * 1.5 );
794 $hp20['height'] = $hp['height'] * 2;
795 }
796
797 $thumb15 = $file->transform( $hp15 );
798 $thumb20 = $file->transform( $hp20 );
799 if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
800 $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
801 }
802 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
803 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
804 }
805 }
806 }
807
822 public static function makeBrokenImageLinkObj(
823 $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
824 $time = false, array $handlerParams = [], bool $currentExists = false
825 ) {
826 if ( !$title instanceof LinkTarget ) {
827 wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
828 return "<!-- ERROR -->" . htmlspecialchars( $label );
829 }
830
831 $title = Title::newFromLinkTarget( $title );
832 $services = MediaWikiServices::getInstance();
833 $mainConfig = $services->getMainConfig();
834 $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
835 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
836 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
837 if ( $label == '' ) {
838 $label = $title->getPrefixedText();
839 }
840
841 $html = Html::element( 'span', [
842 'class' => 'mw-file-element mw-broken-media',
843 // These data attributes are used to dynamically size the span, see T273013
844 'data-width' => $handlerParams['width'] ?? null,
845 'data-height' => $handlerParams['height'] ?? null,
846 ], $label );
847
848 $repoGroup = $services->getRepoGroup();
849 $currentExists = $currentExists ||
850 ( $time && $repoGroup->findFile( $title ) !== false );
851
852 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
853 && !$currentExists
854 ) {
855 if (
856 $title->inNamespace( NS_FILE ) &&
857 $repoGroup->getLocalRepo()->checkRedirect( $title )
858 ) {
859 // We already know it's a redirect, so mark it accordingly
860 return self::link(
861 $title,
862 $html,
863 [ 'class' => 'mw-redirect' ],
864 wfCgiToArray( $query ),
865 [ 'known', 'noclasses' ]
866 );
867 }
868 return Html::rawElement( 'a', [
869 'href' => self::getUploadUrl( $title, $query ),
870 'class' => 'new',
871 'title' => $title->getPrefixedText()
872 ], $html );
873 }
874 return self::link(
875 $title,
876 $html,
877 [],
878 wfCgiToArray( $query ),
879 [ 'known', 'noclasses' ]
880 );
881 }
882
891 public static function getUploadUrl( $destFile, $query = '' ) {
892 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
893 $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
894 $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
895 $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
896 if ( $query != '' ) {
897 $q .= '&' . $query;
898 }
899
900 if ( $uploadMissingFileUrl ) {
901 return wfAppendQuery( $uploadMissingFileUrl, $q );
902 }
903
904 if ( $uploadNavigationUrl ) {
905 return wfAppendQuery( $uploadNavigationUrl, $q );
906 }
907
908 $upload = SpecialPage::getTitleFor( 'Upload' );
909
910 return $upload->getLocalURL( $q );
911 }
912
922 public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
923 $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
924 $title, [ 'time' => $time ]
925 );
926 return self::makeMediaLinkFile( $title, $img, $html );
927 }
928
941 public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
942 if ( $file && $file->exists() ) {
943 $url = $file->getUrl();
944 $class = 'internal';
945 } else {
946 $url = self::getUploadUrl( $title );
947 $class = 'new';
948 }
949
950 $alt = $title->getText();
951 if ( $html == '' ) {
952 $html = $alt;
953 }
954
955 $ret = '';
956 $attribs = [
957 'href' => $url,
958 'class' => $class,
959 'title' => $alt
960 ];
961
962 if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
963 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
964 ) {
965 wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
966 . "with url {$url} and text {$html} to {$ret}" );
967 return $ret;
968 }
969
970 return Html::rawElement( 'a', $attribs, $html );
971 }
972
983 public static function specialLink( $name, $key = '' ) {
984 $queryPos = strpos( $name, '?' );
985 if ( $queryPos !== false ) {
986 $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
987 $name = substr( $name, 0, $queryPos );
988 } else {
989 $getParams = [];
990 }
991
992 $slashPos = strpos( $name, '/' );
993 if ( $slashPos !== false ) {
994 $subpage = substr( $name, $slashPos + 1 );
995 $name = substr( $name, 0, $slashPos );
996 } else {
997 $subpage = false;
998 }
999
1000 if ( $key == '' ) {
1001 $key = strtolower( $name );
1002 }
1003
1004 return self::linkKnown(
1005 SpecialPage::getTitleFor( $name, $subpage ),
1006 wfMessage( $key )->escaped(),
1007 [],
1008 $getParams
1009 );
1010 }
1011
1032 public static function makeExternalLink( $url, $text, $escape = true,
1033 $linktype = '', $attribs = [], $title = null
1034 ) {
1035 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1036 global $wgTitle;
1037 return self::getLinkRenderer()->makeExternalLink(
1038 $url,
1039 $escape ? $text : new HtmlArmor( $text ),
1040 $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1041 $linktype,
1042 $attribs
1043 );
1044 }
1045
1060 public static function userLink(
1061 $userId,
1062 $userName,
1063 $altUserName = false,
1064 $attributes = []
1065 ) {
1066 if ( $userName === '' || $userName === false || $userName === null ) {
1067 wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1068 'that need to be fixed?' );
1069 return wfMessage( 'empty-username' )->parse();
1070 }
1071
1072 return self::getLinkRenderer()->makeUserLink(
1073 new UserIdentityValue( $userId, (string)$userName ),
1074 RequestContext::getMain(),
1075 $altUserName === false ? null : (string)$altUserName,
1076 $attributes
1077 );
1078 }
1079
1098 public static function userToolLinkArray(
1099 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1100 ): array {
1101 $services = MediaWikiServices::getInstance();
1102 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1103 $talkable = !( $disableAnonTalk && $userId == 0 );
1104 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1105 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1106
1107 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1108 // No tools for an external user
1109 return [];
1110 }
1111
1112 $items = [];
1113 if ( $talkable ) {
1114 $items[] = self::userTalkLink( $userId, $userText );
1115 }
1116
1117 // (T412013) Do not link to Special:Contributions for temp accounts
1118 // since the target for the link in the username itself already links to
1119 // Special:Contributions.
1120 if ( $userId && !$services->getTempUserConfig()->isTempName( $userText ) ) {
1121 $attribs = [];
1122 $attribs['class'] = 'mw-usertoollinks-contribs';
1123
1124 // check if the user has edits
1125 if ( $redContribsWhenNoEdits ) {
1126 if ( $edits === null ) {
1127 $user = UserIdentityValue::newRegistered( $userId, $userText );
1128 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1129 }
1130 if ( $edits === 0 ) {
1131 // Note: "new" class is inappropriate here, as "new" class
1132 // should only be used for pages that do not exist.
1133 $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1134 }
1135 }
1136 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1137
1138 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1139 }
1140 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1141 if ( $blockable && $userCanBlock ) {
1142 $items[] = self::blockLink( $userId, $userText );
1143 }
1144
1145 if (
1146 $addEmailLink
1147 && MediaWikiServices::getInstance()->getEmailUserFactory()
1148 ->newEmailUser( RequestContext::getMain()->getAuthority() )
1149 ->canSend()
1150 ->isGood()
1151 ) {
1152 $items[] = self::emailLink( $userId, $userText );
1153 }
1154
1155 ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1156
1157 return $items;
1158 }
1159
1167 public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1168 global $wgLang;
1169
1170 if ( !$items ) {
1171 return '';
1172 }
1173
1174 if ( $useParentheses ) {
1175 return wfMessage( 'word-separator' )->escaped()
1176 . '<span class="mw-usertoollinks">'
1177 . wfMessage( 'parentheses' )->rawParams( $wgLang->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 $node->attrs['href'] =
1412 $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE ) ?? false;
1413 return $node;
1414 }
1415 );
1416 }
1417
1424 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1425 # Valid link forms:
1426 # Foobar -- normal
1427 # :Foobar -- override special treatment of prefix (images, language links)
1428 # /Foobar -- convert to CurrentPage/Foobar
1429 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1430 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1431 # ../Foobar -- convert to CurrentPage/Foobar,
1432 # (from CurrentPage/CurrentSubPage)
1433 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1434 # (from CurrentPage/CurrentSubPage)
1435
1436 $ret = $target; # default return value is no change
1437
1438 # Some namespaces don't allow subpages,
1439 # so only perform processing if subpages are allowed
1440 if (
1441 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1442 hasSubpages( $contextTitle->getNamespace() )
1443 ) {
1444 $hash = strpos( $target, '#' );
1445 if ( $hash !== false ) {
1446 $suffix = substr( $target, $hash );
1447 $target = substr( $target, 0, $hash );
1448 } else {
1449 $suffix = '';
1450 }
1451 # T9425
1452 $target = trim( $target );
1453 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1454 getPrefixedText( $contextTitle );
1455 # Look at the first character
1456 if ( $target != '' && $target[0] === '/' ) {
1457 # / at end means we don't want the slash to be shown
1458 $m = [];
1459 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1460 if ( $trailingSlashes ) {
1461 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1462 } else {
1463 $noslash = substr( $target, 1 );
1464 }
1465
1466 $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1467 if ( $text === '' ) {
1468 $text = $target . $suffix;
1469 } # this might be changed for ugliness reasons
1470 } else {
1471 # check for .. subpage backlinks
1472 $dotdotcount = 0;
1473 $nodotdot = $target;
1474 while ( str_starts_with( $nodotdot, '../' ) ) {
1475 ++$dotdotcount;
1476 $nodotdot = substr( $nodotdot, 3 );
1477 }
1478 if ( $dotdotcount > 0 ) {
1479 $exploded = explode( '/', $contextPrefixedText );
1480 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1481 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1482 # / at the end means don't show full path
1483 if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1484 $nodotdot = rtrim( $nodotdot, '/' );
1485 if ( $text === '' ) {
1486 $text = $nodotdot . $suffix;
1487 }
1488 }
1489 $nodotdot = trim( $nodotdot );
1490 if ( $nodotdot != '' ) {
1491 $ret .= '/' . $nodotdot;
1492 }
1493 $ret .= $suffix;
1494 }
1495 }
1496 }
1497 }
1498
1499 return $ret;
1500 }
1501
1507 public static function formatRevisionSize( $size ) {
1508 if ( $size == 0 ) {
1509 $stxt = wfMessage( 'historyempty' )->escaped();
1510 } else {
1511 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1512 }
1513 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1514 }
1515
1522 public static function splitTrail( $trail ) {
1523 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1524 $inside = '';
1525 if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1526 [ , $inside, $trail ] = $m;
1527 }
1528 return [ $inside, $trail ];
1529 }
1530
1561 public static function generateRollback(
1562 RevisionRecord $revRecord,
1563 ?IContextSource $context = null,
1564 $options = []
1565 ) {
1566 $context ??= RequestContext::getMain();
1567
1568 $editCount = self::getRollbackEditCount( $revRecord );
1569 if ( $editCount === false ) {
1570 return '';
1571 }
1572
1573 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1574
1575 $services = MediaWikiServices::getInstance();
1576 // Allow extensions to modify the rollback link.
1577 // Abort further execution if the extension wants full control over the link.
1578 if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1579 $revRecord, $context, $options, $inner ) ) {
1580 return $inner;
1581 }
1582
1583 if ( !in_array( 'noBrackets', $options, true ) ) {
1584 $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1585 }
1586
1587 if ( $services->getUserOptionsLookup()
1588 ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1589 ) {
1590 $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1591 }
1592
1593 return '<span class="mw-rollback-link">' . $inner . '</span>';
1594 }
1595
1614 public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1615 if ( func_num_args() > 1 ) {
1616 wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1617 }
1618 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1619 ->get( MainConfigNames::ShowRollbackEditCount );
1620
1621 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1622 // Nothing has happened, indicate this by returning 'null'
1623 return null;
1624 }
1625
1626 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1627
1628 // Up to the value of $wgShowRollbackEditCount revisions are counted
1629 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1630 $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1631 ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1632 ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1633 ->limit( $showRollbackEditCount + 1 )
1634 ->caller( __METHOD__ )->fetchResultSet();
1635
1636 $revUser = $revRecord->getUser( RevisionRecord::RAW );
1637 $revUserText = $revUser ? $revUser->getName() : '';
1638
1639 $editCount = 0;
1640 $moreRevs = false;
1641 foreach ( $res as $row ) {
1642 if ( $row->rev_user_text != $revUserText ) {
1643 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1644 || $row->rev_deleted & RevisionRecord::DELETED_USER
1645 ) {
1646 // If the user or the text of the revision we might rollback
1647 // to is deleted in some way we can't rollback. Similar to
1648 // the checks in WikiPage::commitRollback.
1649 return false;
1650 }
1651 $moreRevs = true;
1652 break;
1653 }
1654 $editCount++;
1655 }
1656
1657 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1658 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1659 // and there weren't any other revisions. That means that the current user is the only
1660 // editor, so we can't rollback
1661 return false;
1662 }
1663 return $editCount;
1664 }
1665
1680 public static function buildRollbackLink(
1681 RevisionRecord $revRecord,
1682 ?IContextSource $context = null,
1683 $editCount = false
1684 ) {
1685 $config = MediaWikiServices::getInstance()->getMainConfig();
1686 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1687 $miserMode = $config->get( MainConfigNames::MiserMode );
1688 // To config which pages are affected by miser mode
1689 $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1690
1691 $context ??= RequestContext::getMain();
1692
1693 $title = $revRecord->getPageAsLinkTarget();
1694 $revUser = $revRecord->getUser();
1695 $revUserText = $revUser ? $revUser->getName() : '';
1696
1697 $query = [
1698 'action' => 'rollback',
1699 'from' => $revUserText,
1700 'token' => $context->getUser()->getEditToken( 'rollback' ),
1701 ];
1702
1703 $attrs = [
1704 'data-mw-interface' => '',
1705 'title' => $context->msg( 'tooltip-rollback' )->text()
1706 ];
1707
1708 $options = [ 'known', 'noclasses' ];
1709
1710 if ( $context->getRequest()->getBool( 'bot' ) ) {
1711 // T17999
1712 $query['hidediff'] = '1';
1713 $query['bot'] = '1';
1714 }
1715
1716 if ( $miserMode ) {
1717 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1718 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1719 $showRollbackEditCount = false;
1720 break;
1721 }
1722 }
1723 }
1724
1725 // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1726 $msg = [ 'rollbacklink' ];
1727 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1728 if ( !is_numeric( $editCount ) ) {
1729 $editCount = self::getRollbackEditCount( $revRecord );
1730 }
1731
1732 if ( $editCount > $showRollbackEditCount ) {
1733 $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1734 } elseif ( $editCount ) {
1735 $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1736 }
1737 }
1738
1739 $html = $context->msg( ...$msg )->parse();
1740 return self::link( $title, $html, $attrs, $query, $options );
1741 }
1742
1751 public static function formatHiddenCategories( $hiddencats ) {
1752 $outText = '';
1753 if ( count( $hiddencats ) > 0 ) {
1754 # Construct the HTML
1755 $outText = '<div class="mw-hiddenCategoriesExplanation">';
1756 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1757 $outText .= "</div><ul>\n";
1758
1759 foreach ( $hiddencats as $titleObj ) {
1760 # If it's hidden, it must exist - no need to check with a LinkBatch
1761 $outText .= '<li>'
1762 . self::link( $titleObj, null, [], [], 'known' )
1763 . "</li>\n";
1764 }
1765 $outText .= '</ul>';
1766 }
1767 return $outText;
1768 }
1769
1773 private static function getContextFromMain() {
1774 $context = RequestContext::getMain();
1775 $context = new DerivativeContext( $context );
1776 return $context;
1777 }
1778
1796 public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1797 if ( !$localizer ) {
1798 $localizer = self::getContextFromMain();
1799 }
1800 $message = $localizer->msg( "tooltip-$name", $msgParams );
1801 // Set a default tooltip for subject namespace tabs if that hasn't
1802 // been defined. See T22126
1803 if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1804 $message = $localizer->msg( 'tooltip-ca-nstab' );
1805 }
1806
1807 if ( $message->isDisabled() ) {
1808 $tooltip = false;
1809 } else {
1810 $tooltip = $message->text();
1811 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1812 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1813 }
1814
1815 $options = (array)$options;
1816
1817 if ( in_array( 'nonexisting', $options ) ) {
1818 $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1819 }
1820 if ( in_array( 'withaccess', $options ) ) {
1821 $accesskey = self::accesskey( $name, $localizer );
1822 if ( $accesskey !== false ) {
1823 // Should be build the same as in jquery.accessKeyLabel.js
1824 if ( $tooltip === false || $tooltip === '' ) {
1825 $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1826 } else {
1827 $tooltip .= $localizer->msg( 'word-separator' )->text();
1828 $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1829 }
1830 }
1831 }
1832
1833 return $tooltip;
1834 }
1835
1837 public static $accesskeycache;
1838
1851 public static function accesskey( $name, $localizer = null ) {
1852 if ( !isset( self::$accesskeycache[$name] ) ) {
1853 if ( !$localizer ) {
1854 $localizer = self::getContextFromMain();
1855 }
1856 $msg = $localizer->msg( "accesskey-$name" );
1857 // Set a default accesskey for subject namespace tabs if an
1858 // accesskey has not been defined. See T22126
1859 if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1860 $msg = $localizer->msg( 'accesskey-ca-nstab' );
1861 }
1862 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1863 }
1864 return self::$accesskeycache[$name];
1865 }
1866
1881 public static function getRevDeleteLink(
1882 Authority $performer,
1883 RevisionRecord $revRecord,
1884 LinkTarget $title
1885 ) {
1886 $canHide = $performer->isAllowed( 'deleterevision' );
1887 $canHideHistory = $performer->isAllowed( 'deletedhistory' );
1888 if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
1889 return '';
1890 }
1891
1892 if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
1893 return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
1894 }
1895 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
1896 getPrefixedDBkey( $title );
1897 if ( $revRecord->getId() ) {
1898 // RevDelete links using revision ID are stable across
1899 // page deletion and undeletion; use when possible.
1900 $query = [
1901 'type' => 'revision',
1902 'target' => $prefixedDbKey,
1903 'ids' => $revRecord->getId()
1904 ];
1905 } else {
1906 // Older deleted entries didn't save a revision ID.
1907 // We have to refer to these by timestamp, ick!
1908 $query = [
1909 'type' => 'archive',
1910 'target' => $prefixedDbKey,
1911 'ids' => $revRecord->getTimestamp()
1912 ];
1913 }
1914 return self::revDeleteLink(
1915 $query,
1916 $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
1917 $canHide
1918 );
1919 }
1920
1933 public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
1934 $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
1935 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1936 $html = wfMessage( $msgKey )->escaped();
1937 $tag = $restricted ? 'strong' : 'span';
1938 $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
1939 return Html::rawElement(
1940 $tag,
1941 [ 'class' => 'mw-revdelundel-link' ],
1942 wfMessage( 'parentheses' )->rawParams( $link )->escaped()
1943 );
1944 }
1945
1957 public static function revDeleteLinkDisabled( $delete = true ) {
1958 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1959 $html = wfMessage( $msgKey )->escaped();
1960 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
1961 return Html::rawElement( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
1962 }
1963
1977 public static function tooltipAndAccesskeyAttribs(
1978 $name,
1979 array $msgParams = [],
1980 $options = null,
1981 $localizer = null
1982 ) {
1983 $options = (array)$options;
1984 $options[] = 'withaccess';
1985
1986 // Get optional parameters from global context if any missing.
1987 if ( !$localizer ) {
1988 $localizer = self::getContextFromMain();
1989 }
1990
1991 $attribs = [
1992 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
1993 'accesskey' => self::accesskey( $name, $localizer )
1994 ];
1995 if ( $attribs['title'] === false ) {
1996 unset( $attribs['title'] );
1997 }
1998 if ( $attribs['accesskey'] === false ) {
1999 unset( $attribs['accesskey'] );
2000 }
2001 return $attribs;
2002 }
2003
2011 public static function tooltip( $name, $options = null ) {
2012 $tooltip = self::titleAttrib( $name, $options );
2013 if ( $tooltip === false ) {
2014 return '';
2015 }
2016 return Html::expandAttributes( [
2017 'title' => $tooltip
2018 ] );
2019 }
2020
2025 private static function getLinkRenderer(
2026 array $legacyOptions = []
2027 ): LinkRenderer {
2028 $services = MediaWikiServices::getInstance();
2029
2030 if ( count( $legacyOptions ) > 0 ) {
2031 return $services->getLinkRendererFactory()->createFromLegacyOptions(
2032 $legacyOptions
2033 );
2034 }
2035
2036 return $services->getLinkRenderer();
2037 }
2038
2039}
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
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
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:1933
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:1837
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:1977
static getUploadUrl( $destFile, $query='')
Get the URL to upload a certain file.
Definition Linker.php:891
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:922
static processResponsiveImages( $file, $thumb, $hp)
Process responsive images: add 1.5x and 2x subimages to the thumbnail, where applicable.
Definition Linker.php:785
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:1561
static buildRollbackLink(RevisionRecord $revRecord, ?IContextSource $context=null, $editCount=false)
Build a raw rollback link, useful for collections of "tool" links.
Definition Linker.php:1680
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1424
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:983
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:1751
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:1614
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1032
static userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:1098
static getImageLinkMTOParams( $frameParams, $query='', $parser=null)
Get the link parameters for MediaTransformOutput::toHtml() from given frame parameters supplied by th...
Definition Linker.php:509
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:1881
static makeThumbLink2(LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query='', array $classes=[], ?Parser $parser=null)
Definition Linker.php:584
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:2011
static makeBrokenImageLinkObj( $title, $label='', $query='', $unused1='', $unused2='', $time=false, array $handlerParams=[], bool $currentExists=false)
Make a "broken" link to an image.
Definition Linker.php:822
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:941
static accesskey( $name, $localizer=null)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1851
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:1796
static renderUserToolLinksArray(array $items, bool $useParentheses)
Generate standard tool links HTML from a link array returned by userToolLinkArray().
Definition Linker.php:1167
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:1522
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:1957
static formatRevisionSize( $size)
Definition Linker.php:1507
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:551
static userLink( $userId, $userName, $altUserName=false, $attributes=[])
Make user link (or user contributions for unregistered users)
Definition Linker.php:1060
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:134
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1147
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.
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)