49use Wikimedia\Assert\Assert;
52use Wikimedia\RemexHtml\Serializer\SerializerNode;
112 $target, $html =
null, $customAttribs = [], $query = [], $options = []
115 wfWarn( __METHOD__ .
': Requires $target to be a LinkTarget object.', 2 );
116 return "<!-- ERROR -->$html";
120 $options = (array)$options;
123 $linkRenderer = $services->getLinkRendererFactory()
124 ->createFromLegacyOptions( $options );
126 $linkRenderer = $services->getLinkRenderer();
129 if ( $html !==
null ) {
135 if ( in_array(
'known', $options,
true ) ) {
136 return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
139 if ( in_array(
'broken', $options,
true ) ) {
140 return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
143 if ( in_array(
'noclasses', $options,
true ) ) {
144 return $linkRenderer->makePreloadedLink( $target, $text,
'', $customAttribs, $query );
147 return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
170 $target, $html =
null, $customAttribs = [],
171 $query = [], $options = [
'known' ]
173 return self::link( $target, $html, $customAttribs, $query, $options );
193 public static function makeSelfLinkObj( $nt, $html =
'', $query =
'', $trail =
'', $prefix =
'', $hash =
'' ) {
194 $nt = Title::newFromLinkTarget( $nt );
197 $attrs[
'class'] =
'mw-selflink-fragment';
198 $attrs[
'href'] =
'#' . $hash;
201 $attrs[
'class'] =
'mw-selflink selflink';
203 $ret = Html::rawElement(
'a', $attrs, $prefix . $html ) . $trail;
205 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
210 $html = htmlspecialchars( $nt->getPrefixedText() );
213 return Html::rawElement(
'a', $attrs, $prefix . $html . $inside ) . $trail;
230 $name = $context->
msg(
'blanknamespace' )->text();
233 getFormattedNsText( $namespace );
235 return $context->
msg(
'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
238 return $context->
msg(
'invalidtitle-unknownnamespace', $namespace, $title )->text();
249 private static function fnamePart(
$url ) {
250 $basename = strrchr(
$url,
'/' );
251 if ( $basename ===
false ) {
254 $basename = substr( $basename, 1 );
271 $alt = self::fnamePart(
$url );
275 ->onLinkerMakeExternalImage(
$url, $alt, $img );
277 wfDebug(
"Hook LinkerMakeExternalImage changed the output of external image "
278 .
"with url {$url} and alt text {$alt} to {$img}" );
328 $file, $frameParams = [], $handlerParams = [], $time =
false,
329 $query =
'', $widthOption =
null
331 $title = Title::newFromLinkTarget( $title );
334 if ( !$hookRunner->onImageBeforeProduceHTML(
null, $title,
336 $file, $frameParams, $handlerParams, $time, $res,
338 $parser, $query, $widthOption )
343 if ( $file && !$file->allowInlineDisplay() ) {
344 wfDebug( __METHOD__ .
': ' . $title->getPrefixedDBkey() .
' does not allow inline display' );
349 $page = $handlerParams[
'page'] ??
false;
350 if ( !isset( $frameParams[
'align'] ) ) {
351 $frameParams[
'align'] =
'';
353 if ( !isset( $frameParams[
'title'] ) ) {
354 $frameParams[
'title'] =
'';
356 if ( !isset( $frameParams[
'class'] ) ) {
357 $frameParams[
'class'] =
'';
361 $config = $services->getMainConfig();
366 !isset( $handlerParams[
'width'] ) &&
367 !isset( $frameParams[
'manualthumb'] ) &&
368 !isset( $frameParams[
'framed'] )
370 $classes[] =
'mw-default-size';
373 $prefix = $postfix =
'';
375 if ( $enableLegacyMediaDOM ) {
376 if ( $frameParams[
'align'] ==
'center' ) {
377 $prefix =
'<div class="center">';
379 $frameParams[
'align'] =
'none';
383 if ( $file && !isset( $handlerParams[
'width'] ) ) {
384 if ( isset( $handlerParams[
'height'] ) && $file->isVectorized() ) {
388 $handlerParams[
'width'] = $svgMaxSize;
390 $handlerParams[
'width'] = $file->getWidth( $page );
393 if ( isset( $frameParams[
'thumbnail'] )
394 || isset( $frameParams[
'manualthumb'] )
395 || isset( $frameParams[
'framed'] )
396 || isset( $frameParams[
'frameless'] )
397 || !$handlerParams[
'width']
401 if ( $widthOption ===
null || !isset( $thumbLimits[$widthOption] ) ) {
402 $userOptionsLookup = $services->getUserOptionsLookup();
407 if ( isset( $frameParams[
'upright'] ) && $frameParams[
'upright'] == 0 ) {
408 $frameParams[
'upright'] = $thumbUpright;
414 $prefWidth = isset( $frameParams[
'upright'] ) ?
415 round( $thumbLimits[$widthOption] * $frameParams[
'upright'], -1 ) :
416 $thumbLimits[$widthOption];
420 if ( !isset( $handlerParams[
'height'] ) && ( $handlerParams[
'width'] <= 0 ||
421 $prefWidth < $handlerParams[
'width'] || $file->isVectorized() ) ) {
422 $handlerParams[
'width'] = $prefWidth;
428 $hasVisibleCaption = isset( $frameParams[
'thumbnail'] ) ||
429 isset( $frameParams[
'manualthumb'] ) ||
430 isset( $frameParams[
'framed'] );
432 if ( $hasVisibleCaption ) {
433 if ( $enableLegacyMediaDOM ) {
438 # Create a thumbnail. Alignment depends on the writing direction of
439 # the page content language (right-aligned for LTR languages,
440 # left-aligned for RTL languages)
441 # If a thumbnail width has not been provided, it is set
442 # to the default user option as specified in Language*.php
443 if ( $frameParams[
'align'] ==
'' ) {
448 $title, $file, $frameParams, $handlerParams, $time, $query,
453 $rdfaType =
'mw:File';
455 if ( isset( $frameParams[
'frameless'] ) ) {
456 $rdfaType .=
'/Frameless';
458 $srcWidth = $file->getWidth( $page );
459 # For "frameless" option: do not present an image bigger than the
460 # source (for bitmap-style images). This is the same behavior as the
461 # "thumb" option does it already.
462 if ( $srcWidth && !$file->mustRender() && $handlerParams[
'width'] > $srcWidth ) {
463 $handlerParams[
'width'] = $srcWidth;
468 if ( $file && isset( $handlerParams[
'width'] ) ) {
469 # Create a resized image, without the additional thumbnail features
470 $thumb = $file->transform( $handlerParams );
475 $isBadFile = $file && $thumb &&
478 if ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
479 $rdfaType =
'mw:Error ' . $rdfaType;
480 $currentExists = $file && $file->exists();
481 if ( $enableLegacyMediaDOM ) {
482 $label = $frameParams[
'title'];
484 if ( $currentExists && !$thumb ) {
485 $label =
wfMessage(
'thumbnail_error',
'' )->text();
486 } elseif ( $thumb && $thumb->isError() ) {
489 'Unknown MediaTransformOutput: ' . get_class( $thumb )
491 $label = $thumb->toText();
493 $label = $frameParams[
'alt'] ??
'';
497 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams, $currentExists
505 if ( isset( $frameParams[
'alt'] ) ) {
506 $params[
'alt'] = $frameParams[
'alt'];
508 $params[
'title'] = $frameParams[
'title'];
509 if ( $enableLegacyMediaDOM ) {
511 'valign' => $frameParams[
'valign'] ??
false,
512 'img-class' => $frameParams[
'class'],
514 if ( isset( $frameParams[
'border'] ) ) {
515 $params[
'img-class'] .= (
$params[
'img-class'] !==
'' ?
' ' :
'' ) .
'thumbborder';
519 'img-class' =>
'mw-file-element',
523 $s = $thumb->toHtml(
$params );
526 if ( $enableLegacyMediaDOM ) {
527 if ( $frameParams[
'align'] !=
'' ) {
528 $s = Html::rawElement(
530 [
'class' =>
'float' . $frameParams[
'align'] ],
534 return str_replace(
"\n",
' ', $prefix . $s . $postfix );
540 if ( $frameParams[
'align'] !=
'' ) {
543 $classes[] =
"mw-halign-{$frameParams['align']}";
544 $caption = Html::rawElement(
545 'figcaption', [], $frameParams[
'caption'] ??
''
547 } elseif ( isset( $frameParams[
'valign'] ) ) {
551 $classes[] =
"mw-valign-{$frameParams['valign']}";
554 if ( isset( $frameParams[
'border'] ) ) {
555 $classes[] =
'mw-image-border';
558 if ( isset( $frameParams[
'class'] ) ) {
559 $classes[] = $frameParams[
'class'];
564 'typeof' => $rdfaType,
567 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
569 return str_replace(
"\n",
' ', $s );
582 if ( isset( $frameParams[
'link-url'] ) && $frameParams[
'link-url'] !==
'' ) {
583 $mtoParams[
'custom-url-link'] = $frameParams[
'link-url'];
584 if ( isset( $frameParams[
'link-target'] ) ) {
585 $mtoParams[
'custom-target-link'] = $frameParams[
'link-target'];
588 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams[
'link-url'] );
589 foreach ( $extLinkAttrs as $name => $val ) {
591 $mtoParams[
'parser-extlink-' . $name] = $val;
594 } elseif ( isset( $frameParams[
'link-title'] ) && $frameParams[
'link-title'] !==
'' ) {
596 $mtoParams[
'custom-title-link'] = Title::newFromLinkTarget(
597 $linkRenderer->normalizeTarget( $frameParams[
'link-title'] )
599 if ( isset( $frameParams[
'link-title-query'] ) ) {
600 $mtoParams[
'custom-title-link-query'] = $frameParams[
'link-title-query'];
602 } elseif ( !empty( $frameParams[
'no-link'] ) ) {
605 $mtoParams[
'desc-link'] =
true;
606 $mtoParams[
'desc-query'] = $query;
624 LinkTarget $title, $file, $label =
'', $alt =
'', $align =
null,
625 $params = [], $framed =
false, $manualthumb =
''
633 if ( $manualthumb ) {
634 $frameParams[
'manualthumb'] = $manualthumb;
635 } elseif ( $framed ) {
636 $frameParams[
'framed'] =
true;
637 } elseif ( !isset(
$params[
'width'] ) ) {
638 $classes[] =
'mw-default-size';
641 $title, $file, $frameParams,
$params,
false,
'', $classes
657 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
658 $time =
false, $query =
'', array $classes = [], ?
Parser $parser =
null
660 $exists = $file && $file->exists();
665 $page = $handlerParams[
'page'] ??
false;
666 $lang = $handlerParams[
'lang'] ??
false;
668 if ( !isset( $frameParams[
'align'] ) ) {
669 $frameParams[
'align'] =
'';
670 if ( $enableLegacyMediaDOM ) {
671 $frameParams[
'align'] =
'right';
674 if ( !isset( $frameParams[
'caption'] ) ) {
675 $frameParams[
'caption'] =
'';
678 if ( empty( $handlerParams[
'width'] ) ) {
680 $handlerParams[
'width'] = isset( $frameParams[
'upright'] ) ? 130 : 180;
685 $manualthumb =
false;
687 $rdfaType =
'mw:File/Thumb';
691 if ( !isset( $frameParams[
'manualthumb'] ) && isset( $frameParams[
'framed'] ) ) {
692 $rdfaType =
'mw:File/Frame';
694 $outerWidth = $handlerParams[
'width'] + 2;
696 if ( isset( $frameParams[
'manualthumb'] ) ) {
697 # Use manually specified thumbnail
698 $manual_title = Title::makeTitleSafe(
NS_FILE, $frameParams[
'manualthumb'] );
699 if ( $manual_title ) {
700 $manual_img = $services->getRepoGroup()
701 ->findFile( $manual_title );
703 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
708 $srcWidth = $file->getWidth( $page );
709 if ( isset( $frameParams[
'framed'] ) ) {
710 $rdfaType =
'mw:File/Frame';
711 if ( !$file->isVectorized() ) {
717 $handlerParams[
'width'] = $srcWidth;
723 if ( $srcWidth && !$file->mustRender() && $handlerParams[
'width'] > $srcWidth ) {
724 $handlerParams[
'width'] = $srcWidth;
728 ? $file->getUnscaledThumb( $handlerParams )
729 : $file->transform( $handlerParams );
733 $outerWidth = $thumb->getWidth() + 2;
735 $outerWidth = $handlerParams[
'width'] + 2;
739 if ( !$enableLegacyMediaDOM && $parser && $rdfaType ===
'mw:File/Thumb' ) {
740 $parser->getOutput()->addModules( [
'mediawiki.page.media' ] );
743 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
744 $linkTitleQuery = [];
745 if ( $page || $lang ) {
747 $linkTitleQuery[
'page'] = $page;
750 $linkTitleQuery[
'lang'] = $lang;
752 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
753 # So we don't need to pass it here in $query. However, the URL for the
754 # zoom icon still needs it, so we make a unique query for it. See T16771
759 && !isset( $frameParams[
'link-title'] )
760 && !isset( $frameParams[
'link-url'] )
761 && !isset( $frameParams[
'no-link'] ) ) {
762 $frameParams[
'link-title'] = $title;
763 $frameParams[
'link-title-query'] = $linkTitleQuery;
766 if ( $frameParams[
'align'] !=
'' ) {
768 $classes[] =
"mw-halign-{$frameParams['align']}";
771 if ( isset( $frameParams[
'class'] ) ) {
772 $classes[] = $frameParams[
'class'];
777 if ( $enableLegacyMediaDOM ) {
778 $s .=
"<div class=\"thumb t{$frameParams['align']}\">"
779 .
"<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
782 $isBadFile = $exists && $thumb && $parser &&
783 $parser->getBadFileLookup()->isBadFile(
784 $manualthumb ? $manual_title : $title->
getDBkey(),
789 $rdfaType =
'mw:Error ' . $rdfaType;
791 if ( !$enableLegacyMediaDOM ) {
792 $label = $frameParams[
'alt'] ??
'';
795 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams,
false
798 } elseif ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
799 $rdfaType =
'mw:Error ' . $rdfaType;
800 if ( $enableLegacyMediaDOM ) {
802 $s .=
wfMessage(
'thumbnail_error',
'' )->escaped();
805 $title,
'',
'',
'',
'', (
bool)$time, $handlerParams,
true
809 if ( $thumb && $thumb->isError() ) {
812 'Unknown MediaTransformOutput: ' . get_class( $thumb )
814 $label = $thumb->toText();
815 } elseif ( !$thumb ) {
816 $label =
wfMessage(
'thumbnail_error',
'' )->text();
821 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams,
true
826 if ( !$noscale && !$manualthumb ) {
833 if ( isset( $frameParams[
'alt'] ) ) {
834 $params[
'alt'] = $frameParams[
'alt'];
836 if ( $enableLegacyMediaDOM ) {
838 'img-class' => ( isset( $frameParams[
'class'] ) && $frameParams[
'class'] !==
''
839 ? $frameParams[
'class'] .
' '
840 :
'' ) .
'thumbimage'
844 'img-class' =>
'mw-file-element',
847 if ( $rdfaType ===
'mw:File/Thumb' ) {
852 $s .= $thumb->toHtml(
$params );
853 if ( isset( $frameParams[
'framed'] ) ) {
856 $zoomIcon = Html::rawElement(
'div', [
'class' =>
'magnify' ],
857 Html::rawElement(
'a', [
859 'class' =>
'internal',
860 'title' =>
wfMessage(
'thumbnail-more' )->text(),
866 if ( $enableLegacyMediaDOM ) {
867 $s .=
' <div class="thumbcaption">' . $zoomIcon . $frameParams[
'caption'] .
'</div></div></div>';
868 return str_replace(
"\n",
' ', $s );
871 $s .= Html::rawElement(
872 'figcaption', [], $frameParams[
'caption'] ??
''
877 'typeof' => $rdfaType,
880 $s = Html::rawElement(
'figure', $attribs, $s );
882 return str_replace(
"\n",
' ', $s );
895 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
897 $hp15[
'width'] = round( $hp[
'width'] * 1.5 );
899 $hp20[
'width'] = $hp[
'width'] * 2;
900 if ( isset( $hp[
'height'] ) ) {
901 $hp15[
'height'] = round( $hp[
'height'] * 1.5 );
902 $hp20[
'height'] = $hp[
'height'] * 2;
905 $thumb15 = $file->transform( $hp15 );
906 $thumb20 = $file->transform( $hp20 );
907 if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
908 $thumb->responsiveUrls[
'1.5'] = $thumb15->getUrl();
910 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
911 $thumb->responsiveUrls[
'2'] = $thumb20->getUrl();
931 $title, $label =
'', $query =
'', $unused1 =
'', $unused2 =
'',
932 $time =
false, array $handlerParams = [],
bool $currentExists =
false
935 wfWarn( __METHOD__ .
': Requires $title to be a LinkTarget object.' );
936 return "<!-- ERROR -->" . htmlspecialchars( $label );
939 $title = Title::newFromLinkTarget( $title );
941 $mainConfig = $services->getMainConfig();
945 if ( $label ==
'' ) {
946 $label = $title->getPrefixedText();
950 'class' =>
'mw-file-element mw-broken-media',
952 'data-width' => $handlerParams[
'width'] ??
null,
953 'data-height' => $handlerParams[
'height'] ??
null,
957 $html = htmlspecialchars( $label, ENT_COMPAT );
960 $repoGroup = $services->getRepoGroup();
961 $currentExists = $currentExists ||
962 ( $time && $repoGroup->findFile( $title ) !== false );
964 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
968 $title->inNamespace(
NS_FILE ) &&
969 $repoGroup->getLocalRepo()->checkRedirect( $title )
975 [
'class' =>
'mw-redirect' ],
977 [
'known',
'noclasses' ]
980 return Html::rawElement(
'a', [
981 'href' => self::getUploadUrl( $title, $query ),
983 'title' => $title->getPrefixedText()
991 [
'known',
'noclasses' ]
1007 $q =
'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
1008 if ( $query !=
'' ) {
1012 if ( $uploadMissingFileUrl ) {
1016 if ( $uploadNavigationUrl ) {
1022 return $upload->getLocalURL( $q );
1036 $title, [
'time' => $time ]
1054 if ( $file && $file->exists() ) {
1055 $url = $file->getUrl();
1056 $class =
'internal';
1063 if ( $html ==
'' ) {
1075 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
1077 wfDebug(
"Hook LinkerMakeMediaLinkFile changed the output of link "
1078 .
"with url {$url} and text {$html} to {$ret}" );
1082 return Html::rawElement(
'a', $attribs, $html );
1096 $queryPos = strpos( $name,
'?' );
1097 if ( $queryPos !==
false ) {
1098 $getParams =
wfCgiToArray( substr( $name, $queryPos + 1 ) );
1099 $name = substr( $name, 0, $queryPos );
1104 $slashPos = strpos( $name,
'/' );
1105 if ( $slashPos !==
false ) {
1106 $subpage = substr( $name, $slashPos + 1 );
1107 $name = substr( $name, 0, $slashPos );
1113 $key = strtolower( $name );
1145 $linktype =
'', $attribs = [], $title =
null
1150 return $linkRenderer->makeExternalLink(
1152 $escape ? $text :
new HtmlArmor( $text ),
1174 $altUserName =
false,
1177 if ( $userName ===
'' || $userName ===
false || $userName ===
null ) {
1178 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1179 'that need to be fixed?' );
1180 return wfMessage(
'empty-username' )->parse();
1183 $classes =
'mw-userlink';
1185 $classes .=
' mw-tempuserlink';
1187 } elseif ( $userId == 0 ) {
1188 $page = ExternalUserNames::getUserLinkTitle( $userName );
1190 if ( ExternalUserNames::isExternal( $userName ) ) {
1191 $classes .=
' mw-extuserlink';
1192 } elseif ( $altUserName ===
false ) {
1193 $altUserName = IPUtils::prettifyIP( $userName );
1195 $classes .=
' mw-anonuserlink';
1197 $page = TitleValue::tryNew(
NS_USER, strtr( $userName,
' ',
'_' ) );
1202 '<bdi>' . htmlspecialchars( $altUserName !==
false ? $altUserName : $userName ) .
'</bdi>';
1204 if ( isset( $attributes[
'class'] ) ) {
1205 $attributes[
'class'] .=
' ' . $classes;
1207 $attributes[
'class'] = $classes;
1211 ?
self::link( $page, $linkText, $attributes )
1212 : Html::rawElement(
'span', $attributes, $linkText );
1234 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null
1238 $talkable = !( $disableAnonTalk && $userId == 0 );
1240 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1242 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1254 $attribs[
'class'] =
'mw-usertoollinks-contribs';
1255 if ( $redContribsWhenNoEdits ) {
1256 if ( $edits ===
null ) {
1257 $user = UserIdentityValue::newRegistered( $userId, $userText );
1258 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1260 if ( $edits === 0 ) {
1263 $attribs[
'class'] .=
' mw-usertoollinks-contribs-no-edits';
1270 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed(
'block' );
1271 if ( $blockable && $userCanBlock ) {
1278 ->newEmailUser( RequestContext::getMain()->
getAuthority() )
1285 (
new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1304 if ( $useParentheses ) {
1305 return wfMessage(
'word-separator' )->escaped()
1306 .
'<span class="mw-usertoollinks">'
1307 .
wfMessage(
'parentheses' )->rawParams(
$wgLang->pipeList( $items ) )->escaped()
1312 foreach ( $items as $tool ) {
1313 $tools[] = Html::rawElement(
'span', [], $tool );
1315 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1316 implode(
' ', $tools ) .
'</span>';
1334 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null,
1335 $useParentheses =
true
1337 if ( $userText ===
'' ) {
1338 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1339 'that need to be fixed?' );
1340 return ' ' .
wfMessage(
'empty-username' )->parse();
1343 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1344 return self::renderUserToolLinksArray( $items, $useParentheses );
1357 $userId, $userText, $edits =
null, $useParentheses =
true
1359 return self::userToolLinks( $userId, $userText,
true, 0, $edits, $useParentheses );
1369 if ( $userText ===
'' ) {
1370 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1371 'that need to be fixed?' );
1372 return wfMessage(
'empty-username' )->parse();
1375 $userTalkPage = TitleValue::tryNew(
NS_USER_TALK, strtr( $userText,
' ',
'_' ) );
1376 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-talk' ];
1377 $linkText =
wfMessage(
'talkpagelinktext' )->escaped();
1379 return $userTalkPage
1380 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1381 : Html::rawElement(
'span', $moreLinkAttribs, $linkText );
1391 if ( $userText ===
'' ) {
1392 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1393 'that need to be fixed?' );
1394 return wfMessage(
'empty-username' )->parse();
1397 $blockPage = SpecialPage::getTitleFor(
'Block', $userText );
1398 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-block' ];
1400 return self::link( $blockPage,
1412 if ( $userText ===
'' ) {
1413 wfLogWarning( __METHOD__ .
' received an empty username. Are there database errors ' .
1414 'that need to be fixed?' );
1415 return wfMessage(
'empty-username' )->parse();
1418 $emailPage = SpecialPage::getTitleFor(
'Emailuser', $userText );
1419 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-mail' ];
1420 return self::link( $emailPage,
1439 $authority = RequestContext::getMain()->getAuthority();
1441 $revUser = $revRecord->
getUser(
1442 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1446 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1449 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1452 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1453 $class = self::getRevisionDeletedClass( $revRecord );
1454 return '<span class="' . $class .
'">' . $link .
'</span>';
1466 $class =
'history-deleted';
1467 if ( $revisionRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1468 $class .=
' mw-history-suppressed';
1488 $useParentheses =
true
1491 $authority = RequestContext::getMain()->getAuthority();
1493 $revUser = $revRecord->
getUser(
1494 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1498 $link = self::userLink(
1500 $revUser->getName(),
1502 [
'data-mw-revid' => $revRecord->
getId() ]
1503 ) . self::userToolLinks(
1505 $revUser->getName(),
1513 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1516 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1517 $class = self::getRevisionDeletedClass( $revRecord );
1518 return ' <span class="' . $class .
' mw-userlink">' . $link .
'</span>';
1534 return HtmlHelper::modifyElements(
1536 static function ( SerializerNode $node ):
bool {
1537 return $node->name ===
'a' && isset( $node->attrs[
'href'] );
1539 static function ( SerializerNode $node ): SerializerNode {
1540 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1541 $node->attrs[
'href'] =
1542 $urlUtils->expand( $node->attrs[
'href'],
PROTO_RELATIVE ) ??
false;
1557 # :Foobar -- override special treatment of prefix (images, language links)
1558 # /Foobar -- convert to CurrentPage/Foobar
1559 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1560 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1561 # ../Foobar -- convert to CurrentPage/Foobar,
1562 # (from CurrentPage/CurrentSubPage)
1563 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1564 # (from CurrentPage/CurrentSubPage)
1566 $ret = $target; #
default return value is no change
1568 # Some namespaces don't allow subpages,
1569 # so only perform processing if subpages are allowed
1571 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1572 hasSubpages( $contextTitle->getNamespace() )
1574 $hash = strpos( $target,
'#' );
1575 if ( $hash !==
false ) {
1576 $suffix = substr( $target, $hash );
1577 $target = substr( $target, 0, $hash );
1582 $target = trim( $target );
1583 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1584 getPrefixedText( $contextTitle );
1585 # Look at the first character
1586 if ( $target !=
'' && $target[0] ===
'/' ) {
1587 # / at end means we don't want the slash to be shown
1589 $trailingSlashes = preg_match_all(
'%(/+)$%', $target, $m );
1590 if ( $trailingSlashes ) {
1591 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1593 $noslash = substr( $target, 1 );
1596 $ret = $contextPrefixedText .
'/' . trim( $noslash ) . $suffix;
1597 if ( $text ===
'' ) {
1598 $text = $target . $suffix;
1599 } #
this might be changed
for ugliness reasons
1601 # check for .. subpage backlinks
1603 $nodotdot = $target;
1604 while ( str_starts_with( $nodotdot,
'../' ) ) {
1606 $nodotdot = substr( $nodotdot, 3 );
1608 if ( $dotdotcount > 0 ) {
1609 $exploded = explode(
'/', $contextPrefixedText );
1610 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1611 $ret = implode(
'/', array_slice( $exploded, 0, -$dotdotcount ) );
1612 # / at the end means don't show full path
1613 if ( substr( $nodotdot, -1, 1 ) ===
'/' ) {
1614 $nodotdot = rtrim( $nodotdot,
'/' );
1615 if ( $text ===
'' ) {
1616 $text = $nodotdot . $suffix;
1619 $nodotdot = trim( $nodotdot );
1620 if ( $nodotdot !=
'' ) {
1621 $ret .=
'/' . $nodotdot;
1639 $stxt =
wfMessage(
'historyempty' )->escaped();
1641 $stxt =
wfMessage(
'nbytes' )->numParams( $size )->escaped();
1643 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1653 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1655 if ( $trail !==
'' && preg_match( $regex, $trail, $m ) ) {
1656 [ , $inside, $trail ] = $m;
1658 return [ $inside, $trail ];
1696 $context ??= RequestContext::getMain();
1698 $editCount = self::getRollbackEditCount( $revRecord );
1699 if ( $editCount ===
false ) {
1703 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1705 $services = MediaWikiServices::getInstance();
1708 if ( !(
new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1709 $revRecord, $context, $options, $inner ) ) {
1713 if ( !in_array(
'noBrackets', $options,
true ) ) {
1714 $inner = $context->msg(
'brackets' )->rawParams( $inner )->escaped();
1717 if ( $services->getUserOptionsLookup()
1718 ->getBoolOption( $context->getUser(),
'showrollbackconfirmation' )
1720 $services->getStatsFactory()
1721 ->getCounter(
'rollbackconfirmation_event_load_total' )
1722 ->copyToStatsdAt(
'rollbackconfirmation.event.load' )
1724 $context->getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1727 return '<span class="mw-rollback-link">' . $inner .
'</span>';
1749 if ( func_num_args() > 1 ) {
1750 wfDeprecated( __METHOD__ .
' with $verify parameter',
'1.40' );
1752 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1753 ->get( MainConfigNames::ShowRollbackEditCount );
1755 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1760 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1763 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1764 $res = $queryBuilder->where( [
'rev_page' => $revRecord->
getPageId() ] )
1765 ->useIndex( [
'revision' =>
'rev_page_timestamp' ] )
1766 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_DESC )
1767 ->limit( $showRollbackEditCount + 1 )
1768 ->caller( __METHOD__ )->fetchResultSet();
1770 $revUser = $revRecord->
getUser( RevisionRecord::RAW );
1771 $revUserText = $revUser ? $revUser->getName() :
'';
1775 foreach ( $res as $row ) {
1776 if ( $row->rev_user_text != $revUserText ) {
1777 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1778 || $row->rev_deleted & RevisionRecord::DELETED_USER
1791 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1819 $config = MediaWikiServices::getInstance()->getMainConfig();
1820 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1821 $miserMode = $config->get( MainConfigNames::MiserMode );
1823 $disableRollbackEditCountSpecialPage = [
'Recentchanges',
'Watchlist' ];
1825 $context ??= RequestContext::getMain();
1828 $revUser = $revRecord->
getUser();
1829 $revUserText = $revUser ? $revUser->getName() :
'';
1832 'action' =>
'rollback',
1833 'from' => $revUserText,
1834 'token' => $context->getUser()->getEditToken(
'rollback' ),
1838 'data-mw' =>
'interface',
1839 'title' => $context->msg(
'tooltip-rollback' )->text()
1842 $options = [
'known',
'noclasses' ];
1844 if ( $context->getRequest()->getBool(
'bot' ) ) {
1846 $query[
'hidediff'] =
'1';
1847 $query[
'bot'] =
'1';
1851 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1852 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1853 $showRollbackEditCount =
false;
1860 $msg = [
'rollbacklink' ];
1861 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1862 if ( !is_numeric( $editCount ) ) {
1863 $editCount = self::getRollbackEditCount( $revRecord );
1866 if ( $editCount > $showRollbackEditCount ) {
1867 $msg = [
'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1868 } elseif ( $editCount ) {
1869 $msg = [
'rollbacklinkcount', Message::numParam( $editCount ) ];
1873 $html = $context->msg( ...$msg )->parse();
1874 return self::link( $title, $html, $attrs, $query, $options );
1887 if ( count( $hiddencats ) > 0 ) {
1888 # Construct the HTML
1889 $outText =
'<div class="mw-hiddenCategoriesExplanation">';
1890 $outText .=
wfMessage(
'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1891 $outText .=
"</div><ul>\n";
1893 foreach ( $hiddencats as $titleObj ) {
1894 # If it's hidden, it must exist - no need to check with a LinkBatch
1896 . self::link( $titleObj,
null, [], [],
'known' )
1899 $outText .=
'</ul>';
1907 private static function getContextFromMain() {
1908 $context = RequestContext::getMain();
1930 public static function titleAttrib( $name, $options =
null, array $msgParams = [], $localizer =
null ) {
1931 if ( !$localizer ) {
1932 $localizer = self::getContextFromMain();
1934 $message = $localizer->msg(
"tooltip-$name", $msgParams );
1937 if ( !$message->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
1938 $message = $localizer->msg(
'tooltip-ca-nstab' );
1941 if ( $message->isDisabled() ) {
1944 $tooltip = $message->text();
1945 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1946 $tooltip = preg_replace(
"/ ?\[alt-.\]$/",
'', $tooltip );
1949 $options = (array)$options;
1951 if ( in_array(
'nonexisting', $options ) ) {
1952 $tooltip = $localizer->msg(
'red-link-title', $tooltip ?:
'' )->text();
1954 if ( in_array(
'withaccess', $options ) ) {
1955 $accesskey = self::accesskey( $name, $localizer );
1956 if ( $accesskey !==
false ) {
1958 if ( $tooltip ===
false || $tooltip ===
'' ) {
1959 $tooltip = $localizer->msg(
'brackets', $accesskey )->text();
1961 $tooltip .= $localizer->msg(
'word-separator' )->text();
1962 $tooltip .= $localizer->msg(
'brackets', $accesskey )->text();
1985 public static function accesskey( $name, $localizer =
null ) {
1986 if ( !isset( self::$accesskeycache[$name] ) ) {
1987 if ( !$localizer ) {
1988 $localizer = self::getContextFromMain();
1990 $msg = $localizer->msg(
"accesskey-$name" );
1993 if ( !$msg->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
1994 $msg = $localizer->msg(
'accesskey-ca-nstab' );
1996 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1998 return self::$accesskeycache[$name];
2020 $canHide = $performer->
isAllowed(
'deleterevision' );
2021 $canHideHistory = $performer->
isAllowed(
'deletedhistory' );
2022 if ( !$canHide && !( $revRecord->
getVisibility() && $canHideHistory ) ) {
2026 if ( !$revRecord->
userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2027 return self::revDeleteLinkDisabled( $canHide );
2029 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2030 getPrefixedDBkey( $title );
2031 if ( $revRecord->
getId() ) {
2035 'type' =>
'revision',
2036 'target' => $prefixedDbKey,
2037 'ids' => $revRecord->
getId()
2043 'type' =>
'archive',
2044 'target' => $prefixedDbKey,
2048 return self::revDeleteLink(
2050 $revRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2067 public static function revDeleteLink( $query = [], $restricted =
false, $delete =
true ) {
2068 $sp = SpecialPage::getTitleFor(
'Revisiondelete' );
2069 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
2070 $html =
wfMessage( $msgKey )->escaped();
2071 $tag = $restricted ?
'strong' :
'span';
2072 $link = self::link( $sp, $html, [], $query, [
'known',
'noclasses' ] );
2075 [
'class' =>
'mw-revdelundel-link' ],
2076 wfMessage(
'parentheses' )->rawParams( $link )->escaped()
2092 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
2093 $html =
wfMessage( $msgKey )->escaped();
2094 $htmlParentheses =
wfMessage(
'parentheses' )->rawParams( $html )->escaped();
2095 return Xml::tags(
'span', [
'class' =>
'mw-revdelundel-link' ], $htmlParentheses );
2113 array $msgParams = [],
2117 $options = (array)$options;
2118 $options[] =
'withaccess';
2121 if ( !$localizer ) {
2122 $localizer = self::getContextFromMain();
2126 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2127 'accesskey' => self::accesskey( $name, $localizer )
2129 if ( $attribs[
'title'] ===
false ) {
2130 unset( $attribs[
'title'] );
2132 if ( $attribs[
'accesskey'] ===
false ) {
2133 unset( $attribs[
'accesskey'] );
2145 public static function tooltip( $name, $options =
null ) {
2146 $tooltip = self::titleAttrib( $name, $options );
2147 if ( $tooltip ===
false ) {
2150 return Xml::expandAttributes( [
2158class_alias( Linker::class,
'Linker' );
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
array $params
The job parameters.
Implements some public methods and some protected utility functions which are required by multiple ch...
Marks HTML that shouldn't be escaped.
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.
A class containing constants representing the names of configuration variables.
const UploadNavigationUrl
Name constant for the UploadNavigationUrl setting, for use with Config::get()
const ThumbUpright
Name constant for the ThumbUpright setting, for use with Config::get()
const EnableUploads
Name constant for the EnableUploads setting, for use with Config::get()
const SVGMaxSize
Name constant for the SVGMaxSize setting, for use with Config::get()
const ResponsiveImages
Name constant for the ResponsiveImages setting, for use with Config::get()
const DisableAnonTalk
Name constant for the DisableAnonTalk setting, for use with Config::get()
const ParserEnableLegacyMediaDOM
Name constant for the ParserEnableLegacyMediaDOM setting, for use with Config::get()
const ThumbLimits
Name constant for the ThumbLimits setting, for use with Config::get()
const UploadMissingFileUrl
Name constant for the UploadMissingFileUrl setting, for use with Config::get()
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,...
static getTitleValueFor( $name, $subpage=false, $fragment='')
Get a localised TitleValue object for a specified special page name.
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.