49use Wikimedia\Assert\Assert;
51use Wikimedia\Parsoid\Core\TOCData;
53use Wikimedia\RemexHtml\Serializer\SerializerNode;
115 $target, $html =
null, $customAttribs = [], $query = [], $options = []
118 wfWarn( __METHOD__ .
': Requires $target to be a LinkTarget object.', 2 );
119 return "<!-- ERROR -->$html";
123 $options = (array)$options;
126 $linkRenderer = $services->getLinkRendererFactory()
127 ->createFromLegacyOptions( $options );
129 $linkRenderer = $services->getLinkRenderer();
132 if ( $html !==
null ) {
138 if ( in_array(
'known', $options,
true ) ) {
139 return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
142 if ( in_array(
'broken', $options,
true ) ) {
143 return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
146 if ( in_array(
'noclasses', $options,
true ) ) {
147 return $linkRenderer->makePreloadedLink( $target, $text,
'', $customAttribs, $query );
150 return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
173 $target, $html =
null, $customAttribs = [],
174 $query = [], $options = [
'known' ]
176 return self::link( $target, $html, $customAttribs, $query, $options );
196 public static function makeSelfLinkObj( $nt, $html =
'', $query =
'', $trail =
'', $prefix =
'', $hash =
'' ) {
197 $nt = Title::newFromLinkTarget( $nt );
200 $attrs[
'class'] =
'mw-selflink-fragment';
201 $attrs[
'href'] =
'#' . $hash;
204 $attrs[
'class'] =
'mw-selflink selflink';
206 $ret = Html::rawElement(
'a', $attrs, $prefix . $html ) . $trail;
208 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
213 $html = htmlspecialchars( $nt->getPrefixedText() );
216 return Html::rawElement(
'a', $attrs, $prefix . $html . $inside ) . $trail;
233 $name = $context->
msg(
'blanknamespace' )->text();
236 getFormattedNsText( $namespace );
238 return $context->
msg(
'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
241 return $context->
msg(
'invalidtitle-unknownnamespace', $namespace, $title )->text();
252 private static function fnamePart( $url ) {
253 $basename = strrchr( $url,
'/' );
254 if ( $basename ===
false ) {
257 $basename = substr( $basename, 1 );
274 $alt = self::fnamePart( $url );
278 ->onLinkerMakeExternalImage( $url, $alt, $img );
280 wfDebug(
"Hook LinkerMakeExternalImage changed the output of external image "
281 .
"with url {$url} and alt text {$alt} to {$img}" );
284 return Html::element(
'img',
331 $file, $frameParams = [], $handlerParams = [], $time =
false,
332 $query =
'', $widthOption =
null
334 $title = Title::newFromLinkTarget( $title );
338 if ( !$hookRunner->onImageBeforeProduceHTML( $dummy, $title,
340 $file, $frameParams, $handlerParams, $time, $res,
342 $parser, $query, $widthOption )
347 if (
$file && !
$file->allowInlineDisplay() ) {
348 wfDebug( __METHOD__ .
': ' . $title->getPrefixedDBkey() .
' does not allow inline display' );
353 $page = $handlerParams[
'page'] ??
false;
354 if ( !isset( $frameParams[
'align'] ) ) {
355 $frameParams[
'align'] =
'';
357 if ( !isset( $frameParams[
'title'] ) ) {
358 $frameParams[
'title'] =
'';
360 if ( !isset( $frameParams[
'class'] ) ) {
361 $frameParams[
'class'] =
'';
365 $config = $services->getMainConfig();
370 !isset( $handlerParams[
'width'] ) &&
371 !isset( $frameParams[
'manualthumb'] ) &&
372 !isset( $frameParams[
'framed'] )
374 $classes[] =
'mw-default-size';
377 $prefix = $postfix =
'';
379 if ( $enableLegacyMediaDOM ) {
380 if ( $frameParams[
'align'] ==
'center' ) {
381 $prefix =
'<div class="center">';
383 $frameParams[
'align'] =
'none';
387 if (
$file && !isset( $handlerParams[
'width'] ) ) {
388 if ( isset( $handlerParams[
'height'] ) &&
$file->isVectorized() ) {
392 $handlerParams[
'width'] = $svgMaxSize;
394 $handlerParams[
'width'] =
$file->getWidth( $page );
397 if ( isset( $frameParams[
'thumbnail'] )
398 || isset( $frameParams[
'manualthumb'] )
399 || isset( $frameParams[
'framed'] )
400 || isset( $frameParams[
'frameless'] )
401 || !$handlerParams[
'width']
405 if ( $widthOption ===
null || !isset( $thumbLimits[$widthOption] ) ) {
406 $userOptionsLookup = $services->getUserOptionsLookup();
407 $widthOption = $userOptionsLookup->getDefaultOption(
'thumbsize' );
411 if ( isset( $frameParams[
'upright'] ) && $frameParams[
'upright'] == 0 ) {
412 $frameParams[
'upright'] = $thumbUpright;
418 $prefWidth = isset( $frameParams[
'upright'] ) ?
419 round( $thumbLimits[$widthOption] * $frameParams[
'upright'], -1 ) :
420 $thumbLimits[$widthOption];
424 if ( !isset( $handlerParams[
'height'] ) && ( $handlerParams[
'width'] <= 0 ||
425 $prefWidth < $handlerParams[
'width'] ||
$file->isVectorized() ) ) {
426 $handlerParams[
'width'] = $prefWidth;
432 $hasVisibleCaption = isset( $frameParams[
'thumbnail'] ) ||
433 isset( $frameParams[
'manualthumb'] ) ||
434 isset( $frameParams[
'framed'] );
436 if ( $hasVisibleCaption ) {
437 if ( $enableLegacyMediaDOM ) {
442 # Create a thumbnail. Alignment depends on the writing direction of
443 # the page content language (right-aligned for LTR languages,
444 # left-aligned for RTL languages)
445 # If a thumbnail width has not been provided, it is set
446 # to the default user option as specified in Language*.php
447 if ( $frameParams[
'align'] ==
'' ) {
452 $title,
$file, $frameParams, $handlerParams, $time, $query,
457 $rdfaType =
'mw:File';
459 if ( isset( $frameParams[
'frameless'] ) ) {
460 $rdfaType .=
'/Frameless';
462 $srcWidth =
$file->getWidth( $page );
463 # For "frameless" option: do not present an image bigger than the
464 # source (for bitmap-style images). This is the same behavior as the
465 # "thumb" option does it already.
466 if ( $srcWidth && !
$file->mustRender() && $handlerParams[
'width'] > $srcWidth ) {
467 $handlerParams[
'width'] = $srcWidth;
472 if (
$file && isset( $handlerParams[
'width'] ) ) {
473 # Create a resized image, without the additional thumbnail features
474 $thumb =
$file->transform( $handlerParams );
479 $isBadFile =
$file && $thumb &&
482 if ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
483 $rdfaType =
'mw:Error ' . $rdfaType;
485 if ( $enableLegacyMediaDOM ) {
486 $label = $frameParams[
'title'];
488 if ( $currentExists && !$thumb ) {
489 $label =
wfMessage(
'thumbnail_error',
'' )->text();
490 } elseif ( $thumb && $thumb->isError() ) {
493 'Unknown MediaTransformOutput: ' . get_class( $thumb )
495 $label = $thumb->toText();
497 $label = $frameParams[
'alt'] ??
'';
501 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams, $currentExists
509 if ( isset( $frameParams[
'alt'] ) ) {
510 $params[
'alt'] = $frameParams[
'alt'];
512 $params[
'title'] = $frameParams[
'title'];
513 if ( $enableLegacyMediaDOM ) {
515 'valign' => $frameParams[
'valign'] ??
false,
516 'img-class' => $frameParams[
'class'],
518 if ( isset( $frameParams[
'border'] ) ) {
519 $params[
'img-class'] .= ( $params[
'img-class'] !==
'' ?
' ' :
'' ) .
'thumbborder';
523 'img-class' =>
'mw-file-element',
527 $s = $thumb->toHtml( $params );
530 if ( $enableLegacyMediaDOM ) {
531 if ( $frameParams[
'align'] !=
'' ) {
532 $s = Html::rawElement(
534 [
'class' =>
'float' . $frameParams[
'align'] ],
538 return str_replace(
"\n",
' ', $prefix . $s . $postfix );
544 if ( $frameParams[
'align'] !=
'' ) {
547 $classes[] =
"mw-halign-{$frameParams['align']}";
548 $caption = Html::rawElement(
549 'figcaption', [], $frameParams[
'caption'] ??
''
551 } elseif ( isset( $frameParams[
'valign'] ) ) {
555 $classes[] =
"mw-valign-{$frameParams['valign']}";
558 if ( isset( $frameParams[
'border'] ) ) {
559 $classes[] =
'mw-image-border';
562 if ( isset( $frameParams[
'class'] ) ) {
563 $classes[] = $frameParams[
'class'];
568 'typeof' => $rdfaType,
571 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
573 return str_replace(
"\n",
' ', $s );
586 if ( isset( $frameParams[
'link-url'] ) && $frameParams[
'link-url'] !==
'' ) {
587 $mtoParams[
'custom-url-link'] = $frameParams[
'link-url'];
588 if ( isset( $frameParams[
'link-target'] ) ) {
589 $mtoParams[
'custom-target-link'] = $frameParams[
'link-target'];
592 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams[
'link-url'] );
593 foreach ( $extLinkAttrs as $name => $val ) {
595 $mtoParams[
'parser-extlink-' . $name] = $val;
598 } elseif ( isset( $frameParams[
'link-title'] ) && $frameParams[
'link-title'] !==
'' ) {
600 $mtoParams[
'custom-title-link'] = Title::newFromLinkTarget(
601 $linkRenderer->normalizeTarget( $frameParams[
'link-title'] )
603 if ( isset( $frameParams[
'link-title-query'] ) ) {
604 $mtoParams[
'custom-title-link-query'] = $frameParams[
'link-title-query'];
606 } elseif ( !empty( $frameParams[
'no-link'] ) ) {
609 $mtoParams[
'desc-link'] =
true;
610 $mtoParams[
'desc-query'] = $query;
629 $params = [], $framed =
false, $manualthumb =
''
637 if ( $manualthumb ) {
638 $frameParams[
'manualthumb'] = $manualthumb;
639 } elseif ( $framed ) {
640 $frameParams[
'framed'] =
true;
641 } elseif ( !isset( $params[
'width'] ) ) {
642 $classes[] =
'mw-default-size';
645 $title,
$file, $frameParams, $params,
false,
'', $classes
662 $time =
false, $query =
'', array $classes = [], ?
Parser $parser =
null
669 $page = $handlerParams[
'page'] ??
false;
670 $lang = $handlerParams[
'lang'] ??
false;
672 if ( !isset( $frameParams[
'align'] ) ) {
673 $frameParams[
'align'] =
'';
674 if ( $enableLegacyMediaDOM ) {
675 $frameParams[
'align'] =
'right';
678 if ( !isset( $frameParams[
'caption'] ) ) {
679 $frameParams[
'caption'] =
'';
682 if ( empty( $handlerParams[
'width'] ) ) {
684 $handlerParams[
'width'] = isset( $frameParams[
'upright'] ) ? 130 : 180;
689 $manualthumb =
false;
691 $rdfaType =
'mw:File/Thumb';
695 if ( !isset( $frameParams[
'manualthumb'] ) && isset( $frameParams[
'framed'] ) ) {
696 $rdfaType =
'mw:File/Frame';
698 $outerWidth = $handlerParams[
'width'] + 2;
700 if ( isset( $frameParams[
'manualthumb'] ) ) {
701 # Use manually specified thumbnail
702 $manual_title = Title::makeTitleSafe(
NS_FILE, $frameParams[
'manualthumb'] );
703 if ( $manual_title ) {
704 $manual_img = $services->getRepoGroup()
705 ->findFile( $manual_title );
707 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
711 } elseif ( isset( $frameParams[
'framed'] ) ) {
713 $thumb =
$file->getUnscaledThumb( $handlerParams );
715 $rdfaType =
'mw:File/Frame';
717 # Do not present an image bigger than the source, for bitmap-style images
718 # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
719 $srcWidth =
$file->getWidth( $page );
720 if ( $srcWidth && !
$file->mustRender() && $handlerParams[
'width'] > $srcWidth ) {
721 $handlerParams[
'width'] = $srcWidth;
723 $thumb =
$file->transform( $handlerParams );
727 $outerWidth = $thumb->getWidth() + 2;
729 $outerWidth = $handlerParams[
'width'] + 2;
733 if ( !$enableLegacyMediaDOM && $parser && $rdfaType ===
'mw:File/Thumb' ) {
734 $parser->getOutput()->addModules( [
'mediawiki.page.media' ] );
737 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
738 $linkTitleQuery = [];
739 if ( $page || $lang ) {
741 $linkTitleQuery[
'page'] = $page;
744 $linkTitleQuery[
'lang'] = $lang;
746 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
747 # So we don't need to pass it here in $query. However, the URL for the
748 # zoom icon still needs it, so we make a unique query for it. See T16771
753 && !isset( $frameParams[
'link-title'] )
754 && !isset( $frameParams[
'link-url'] )
755 && !isset( $frameParams[
'no-link'] ) ) {
756 $frameParams[
'link-title'] = $title;
757 $frameParams[
'link-title-query'] = $linkTitleQuery;
760 if ( $frameParams[
'align'] !=
'' ) {
762 $classes[] =
"mw-halign-{$frameParams['align']}";
765 if ( isset( $frameParams[
'class'] ) ) {
766 $classes[] = $frameParams[
'class'];
771 if ( $enableLegacyMediaDOM ) {
772 $s .=
"<div class=\"thumb t{$frameParams['align']}\">"
773 .
"<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
776 $isBadFile = $exists && $thumb && $parser &&
777 $parser->getBadFileLookup()->isBadFile(
778 $manualthumb ? $manual_title : $title->
getDBkey(),
783 $rdfaType =
'mw:Error ' . $rdfaType;
785 if ( !$enableLegacyMediaDOM ) {
786 $label = $frameParams[
'alt'] ??
'';
789 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams,
false
792 } elseif ( !$thumb || ( !$enableLegacyMediaDOM && $thumb->isError() ) || $isBadFile ) {
793 $rdfaType =
'mw:Error ' . $rdfaType;
794 if ( $enableLegacyMediaDOM ) {
796 $s .=
wfMessage(
'thumbnail_error',
'' )->escaped();
799 $title,
'',
'',
'',
'', (
bool)$time, $handlerParams,
true
803 if ( $thumb && $thumb->isError() ) {
806 'Unknown MediaTransformOutput: ' . get_class( $thumb )
808 $label = $thumb->toText();
809 } elseif ( !$thumb ) {
810 $label =
wfMessage(
'thumbnail_error',
'' )->text();
815 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams,
true
820 if ( !$noscale && !$manualthumb ) {
827 if ( isset( $frameParams[
'alt'] ) ) {
828 $params[
'alt'] = $frameParams[
'alt'];
830 if ( $enableLegacyMediaDOM ) {
832 'img-class' => ( isset( $frameParams[
'class'] ) && $frameParams[
'class'] !==
''
833 ? $frameParams[
'class'] .
' '
834 :
'' ) .
'thumbimage'
838 'img-class' =>
'mw-file-element',
841 if ( $rdfaType ===
'mw:File/Thumb' ) {
842 $params[
'magnify-resource'] = $url;
846 $s .= $thumb->toHtml( $params );
847 if ( isset( $frameParams[
'framed'] ) ) {
850 $zoomIcon = Html::rawElement(
'div', [
'class' =>
'magnify' ],
851 Html::rawElement(
'a', [
853 'class' =>
'internal',
854 'title' =>
wfMessage(
'thumbnail-more' )->text(),
860 if ( $enableLegacyMediaDOM ) {
861 $s .=
' <div class="thumbcaption">' . $zoomIcon . $frameParams[
'caption'] .
'</div></div></div>';
862 return str_replace(
"\n",
' ', $s );
865 $s .= Html::rawElement(
866 'figcaption', [], $frameParams[
'caption'] ??
''
871 'typeof' => $rdfaType,
874 $s = Html::rawElement(
'figure', $attribs, $s );
876 return str_replace(
"\n",
' ', $s );
889 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
891 $hp15[
'width'] = round( $hp[
'width'] * 1.5 );
893 $hp20[
'width'] = $hp[
'width'] * 2;
894 if ( isset( $hp[
'height'] ) ) {
895 $hp15[
'height'] = round( $hp[
'height'] * 1.5 );
896 $hp20[
'height'] = $hp[
'height'] * 2;
899 $thumb15 =
$file->transform( $hp15 );
900 $thumb20 =
$file->transform( $hp20 );
901 if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
902 $thumb->responsiveUrls[
'1.5'] = $thumb15->getUrl();
904 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
905 $thumb->responsiveUrls[
'2'] = $thumb20->getUrl();
925 $title, $label =
'', $query =
'', $unused1 =
'', $unused2 =
'',
926 $time =
false, array $handlerParams = [],
bool $currentExists =
false
929 wfWarn( __METHOD__ .
': Requires $title to be a LinkTarget object.' );
930 return "<!-- ERROR -->" . htmlspecialchars( $label );
933 $title = Title::newFromLinkTarget( $title );
935 $mainConfig = $services->getMainConfig();
939 if ( $label ==
'' ) {
940 $label = $title->getPrefixedText();
943 $html = Html::element(
'span', [
944 'class' =>
'mw-file-element mw-broken-media',
946 'data-width' => $handlerParams[
'width'] ??
null,
947 'data-height' => $handlerParams[
'height'] ??
null,
951 $html = htmlspecialchars( $label, ENT_COMPAT );
954 $repoGroup = $services->getRepoGroup();
955 $currentExists = $currentExists ||
956 ( $time && $repoGroup->findFile( $title ) !== false );
958 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
962 $title->inNamespace(
NS_FILE ) &&
963 $repoGroup->getLocalRepo()->checkRedirect( $title )
969 [
'class' =>
'mw-redirect' ],
971 [
'known',
'noclasses' ]
974 return Html::rawElement(
'a', [
975 'href' => self::getUploadUrl( $title, $query ),
977 'title' => $title->getPrefixedText()
985 [
'known',
'noclasses' ]
1001 $q =
'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
1002 if ( $query !=
'' ) {
1006 if ( $uploadMissingFileUrl ) {
1010 if ( $uploadNavigationUrl ) {
1016 return $upload->getLocalURL( $q );
1030 $title, [
'time' => $time ]
1049 $url =
$file->getUrl();
1050 $class =
'internal';
1057 if ( $html ==
'' ) {
1069 Title::newFromLinkTarget( $title ),
$file, $html, $attribs, $ret )
1071 wfDebug(
"Hook LinkerMakeMediaLinkFile changed the output of link "
1072 .
"with url {$url} and text {$html} to {$ret}" );
1076 return Html::rawElement(
'a', $attribs, $html );
1090 $queryPos = strpos( $name,
'?' );
1091 if ( $queryPos !==
false ) {
1092 $getParams =
wfCgiToArray( substr( $name, $queryPos + 1 ) );
1093 $name = substr( $name, 0, $queryPos );
1098 $slashPos = strpos( $name,
'/' );
1099 if ( $slashPos !==
false ) {
1100 $subpage = substr( $name, $slashPos + 1 );
1101 $name = substr( $name, 0, $slashPos );
1107 $key = strtolower( $name );
1137 $linktype =
'', $attribs = [], $title =
null
1140 $class =
'external';
1142 $class .=
" $linktype";
1144 if ( isset( $attribs[
'class'] ) && $attribs[
'class'] ) {
1145 $class .=
" {$attribs['class']}";
1147 $attribs[
'class'] = $class;
1150 $text = htmlspecialchars( $text, ENT_COMPAT );
1157 if ( !isset( $attribs[
'rel'] ) || $attribs[
'rel'] ===
'' ) {
1158 $attribs[
'rel'] = $newRel;
1159 } elseif ( $newRel !==
null ) {
1161 $newRels = explode(
' ', $newRel );
1162 $oldRels = explode(
' ', $attribs[
'rel'] );
1163 $combined = array_unique( array_merge( $newRels, $oldRels ) );
1164 $attribs[
'rel'] = implode(
' ', $combined );
1168 $url, $text, $link, $attribs, $linktype );
1170 wfDebug(
"Hook LinkerMakeExternalLink changed the output of link "
1171 .
"with url {$url} and text {$text} to {$link}" );
1174 $attribs[
'href'] = $url;
1175 return Html::rawElement(
'a', $attribs, $text );
1193 $altUserName =
false,
1196 if ( $userName ===
'' || $userName ===
false || $userName ===
null ) {
1197 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1198 'that need to be fixed?' );
1199 return wfMessage(
'empty-username' )->parse();
1202 $classes =
'mw-userlink';
1204 $classes .=
' mw-tempuserlink';
1206 } elseif ( $userId == 0 ) {
1207 $page = ExternalUserNames::getUserLinkTitle( $userName );
1209 if ( ExternalUserNames::isExternal( $userName ) ) {
1210 $classes .=
' mw-extuserlink';
1211 } elseif ( $altUserName ===
false ) {
1212 $altUserName = IPUtils::prettifyIP( $userName );
1214 $classes .=
' mw-anonuserlink';
1216 $page = TitleValue::tryNew(
NS_USER, strtr( $userName,
' ',
'_' ) );
1221 '<bdi>' . htmlspecialchars( $altUserName !==
false ? $altUserName : $userName ) .
'</bdi>';
1223 if ( isset( $attributes[
'class'] ) ) {
1224 $attributes[
'class'] .=
' ' . $classes;
1226 $attributes[
'class'] = $classes;
1230 ?
self::link( $page, $linkText, $attributes )
1231 : Html::rawElement(
'span', $attributes, $linkText );
1249 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null,
1250 $useParentheses =
true
1252 if ( $userText ===
'' ) {
1253 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1254 'that need to be fixed?' );
1255 return ' ' .
wfMessage(
'empty-username' )->parse();
1260 $talkable = !( $disableAnonTalk && $userId == 0 );
1262 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1264 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1276 $attribs[
'class'] =
'mw-usertoollinks-contribs';
1277 if ( $redContribsWhenNoEdits ) {
1278 if ( intval( $edits ) === 0 && $edits !== 0 ) {
1279 $user = User::newFromId( $userId );
1280 $edits = $user->getEditCount();
1282 if ( $edits === 0 ) {
1285 $attribs[
'class'] .=
' mw-usertoollinks-contribs-no-edits';
1292 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed(
'block' );
1293 if ( $blockable && $userCanBlock ) {
1297 $user = RequestContext::getMain()->getUser();
1298 if ( $addEmailLink && $user->canSendEmail() ) {
1302 (
new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1308 if ( $useParentheses ) {
1309 return wfMessage(
'word-separator' )->escaped()
1310 .
'<span class="mw-usertoollinks">'
1311 .
wfMessage(
'parentheses' )->rawParams(
$wgLang->pipeList( $items ) )->escaped()
1316 foreach ( $items as $tool ) {
1317 $tools[] = Html::rawElement(
'span', [], $tool );
1319 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1320 implode(
' ', $tools ) .
'</span>';
1333 $userId, $userText, $edits =
null, $useParentheses =
true
1345 if ( $userText ===
'' ) {
1346 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1347 'that need to be fixed?' );
1348 return wfMessage(
'empty-username' )->parse();
1351 $userTalkPage = TitleValue::tryNew(
NS_USER_TALK, strtr( $userText,
' ',
'_' ) );
1352 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-talk' ];
1353 $linkText =
wfMessage(
'talkpagelinktext' )->escaped();
1355 return $userTalkPage
1356 ?
self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1357 : Html::rawElement(
'span', $moreLinkAttribs, $linkText );
1367 if ( $userText ===
'' ) {
1368 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1369 'that need to be fixed?' );
1370 return wfMessage(
'empty-username' )->parse();
1374 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-block' ];
1388 if ( $userText ===
'' ) {
1389 wfLogWarning( __METHOD__ .
' received an empty username. Are there database errors ' .
1390 'that need to be fixed?' );
1391 return wfMessage(
'empty-username' )->parse();
1395 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-mail' ];
1415 $authority = RequestContext::getMain()->getAuthority();
1417 $revUser = $revRecord->
getUser(
1418 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1422 $link =
self::userLink( $revUser->getId(), $revUser->getName() );
1425 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1428 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1430 return '<span class="' . $class .
'">' . $link .
'</span>';
1442 $class =
'history-deleted';
1443 if ( $revisionRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1444 $class .=
' mw-history-suppressed';
1464 $useParentheses =
true
1467 $authority = RequestContext::getMain()->getAuthority();
1469 $revUser = $revRecord->
getUser(
1470 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1474 $link = self::userLink(
1476 $revUser->getName(),
1478 [
'data-mw-revid' => $revRecord->
getId() ]
1479 ) . self::userToolLinks(
1481 $revUser->getName(),
1489 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1492 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1493 $class = self::getRevisionDeletedClass( $revRecord );
1494 return ' <span class="' . $class .
' mw-userlink">' . $link .
'</span>';
1510 return HtmlHelper::modifyElements(
1512 static function ( SerializerNode $node ):
bool {
1513 return $node->name ===
'a' && isset( $node->attrs[
'href'] );
1515 static function ( SerializerNode $node ): SerializerNode {
1516 $node->attrs[
'href'] =
1544 $comment, $title =
null, $local =
false, $wikiId =
null
1548 return $formatter->format( $comment, $title, $local, $wikiId );
1571 $comment, $title =
null, $local =
false, $wikiId =
null
1575 return $formatter->formatLinksUnsafe( $comment, $title, $local, $wikiId );
1587 # :Foobar -- override special treatment of prefix (images, language links)
1588 # /Foobar -- convert to CurrentPage/Foobar
1589 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1590 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1591 # ../Foobar -- convert to CurrentPage/Foobar,
1592 # (from CurrentPage/CurrentSubPage)
1593 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1594 # (from CurrentPage/CurrentSubPage)
1596 $ret = $target; #
default return value is no change
1598 # Some namespaces don't allow subpages,
1599 # so only perform processing if subpages are allowed
1602 hasSubpages( $contextTitle->getNamespace() )
1604 $hash = strpos( $target,
'#' );
1605 if ( $hash !==
false ) {
1606 $suffix = substr( $target, $hash );
1607 $target = substr( $target, 0, $hash );
1612 $target = trim( $target );
1614 getPrefixedText( $contextTitle );
1615 # Look at the first character
1616 if ( $target !=
'' && $target[0] ===
'/' ) {
1617 # / at end means we don't want the slash to be shown
1619 $trailingSlashes = preg_match_all(
'%(/+)$%', $target, $m );
1620 if ( $trailingSlashes ) {
1621 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1623 $noslash = substr( $target, 1 );
1626 $ret = $contextPrefixedText .
'/' . trim( $noslash ) . $suffix;
1627 if ( $text ===
'' ) {
1628 $text = $target . $suffix;
1629 } #
this might be changed
for ugliness reasons
1631 # check for .. subpage backlinks
1633 $nodotdot = $target;
1634 while ( str_starts_with( $nodotdot,
'../' ) ) {
1636 $nodotdot = substr( $nodotdot, 3 );
1638 if ( $dotdotcount > 0 ) {
1639 $exploded = explode(
'/', $contextPrefixedText );
1640 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1641 $ret = implode(
'/', array_slice( $exploded, 0, -$dotdotcount ) );
1642 # / at the end means don't show full path
1643 if ( substr( $nodotdot, -1, 1 ) ===
'/' ) {
1644 $nodotdot = rtrim( $nodotdot,
'/' );
1645 if ( $text ===
'' ) {
1646 $text = $nodotdot . $suffix;
1649 $nodotdot = trim( $nodotdot );
1650 if ( $nodotdot !=
'' ) {
1651 $ret .=
'/' . $nodotdot;
1682 $comment, $title =
null, $local =
false, $wikiId =
null, $useParentheses =
true
1686 ->formatBlock( $comment, $title, $local, $wikiId, $useParentheses );
1708 $useParentheses =
true
1711 $authority = RequestContext::getMain()->getAuthority();
1713 return $formatter->formatRevision( $revRecord, $authority, $local, $isPublic, $useParentheses );
1723 $stxt =
wfMessage(
'historyempty' )->escaped();
1725 $stxt =
wfMessage(
'nbytes' )->numParams( $size )->escaped();
1727 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1748 return "</li>\n" . str_repeat(
"</ul>\n</li>\n", $level > 0 ? $level : 0 );
1762 public static function tocLine( $linkAnchor, $tocline, $tocnumber, $level, $sectionIndex =
false ) {
1763 $classes =
"toclevel-$level";
1768 if ( $sectionIndex !==
false && $sectionIndex !==
'' && !str_starts_with( $sectionIndex,
"T-" ) ) {
1769 $classes .=
" tocsection-$sectionIndex";
1774 return Html::openElement(
'li', [
'class' => $classes ] )
1775 . Html::rawElement(
'a',
1776 [
'href' =>
"#$linkAnchor" ],
1777 Html::element(
'span', [
'class' =>
'tocnumber' ], $tocnumber )
1779 . Html::rawElement(
'span', [
'class' =>
'toctext' ], $tocline )
1803 $lang ??= RequestContext::getMain()->getLanguage();
1805 $title =
wfMessage(
'toc' )->inLanguage( $lang )->escaped();
1807 return '<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading">'
1808 . Html::element(
'input', [
1809 'type' =>
'checkbox',
1811 'id' =>
'toctogglecheckbox',
1812 'class' =>
'toctogglecheckbox',
1813 'style' =>
'display:none',
1815 . Html::openElement(
'div', [
1816 'class' =>
'toctitle',
1817 'lang' => $lang->getHtmlCode(),
1818 'dir' => $lang->getDir(),
1820 .
'<h2 id="mw-toc-heading">' . $title .
'</h2>'
1821 .
'<span class="toctogglespan">'
1822 . Html::label(
'',
'toctogglecheckbox', [
1823 'class' =>
'toctogglelabel',
1828 .
"</ul>\n</div>\n";
1845 $maxTocLevel = $options[
'maxtoclevel'] ??
null;
1846 if ( $maxTocLevel ===
null ) {
1849 $config = $services->getMainConfig();
1852 foreach ( ( $tocData ? $tocData->getSections() : [] ) as $section ) {
1853 $tocLevel = $section->tocLevel;
1854 if ( $tocLevel < $maxTocLevel ) {
1855 if ( $tocLevel > $lastLevel ) {
1856 $toc .= self::tocIndent();
1857 } elseif ( $tocLevel < $lastLevel ) {
1858 if ( $lastLevel < $maxTocLevel ) {
1859 $toc .= self::tocUnindent(
1860 $lastLevel - $tocLevel );
1862 $toc .= self::tocLineEnd();
1865 $toc .= self::tocLineEnd();
1868 $toc .= self::tocLine( $section->linkAnchor,
1869 $section->line, $section->number,
1870 $tocLevel, $section->index );
1871 $lastLevel = $tocLevel;
1874 if ( $lastLevel < $maxTocLevel && $lastLevel > 0 ) {
1875 $toc .= self::tocUnindent( $lastLevel - 1 );
1877 return self::tocList( $toc, $lang );
1897 $link, $fallbackAnchor =
false
1899 $anchorEscaped = htmlspecialchars( $anchor, ENT_COMPAT );
1901 if ( $fallbackAnchor !==
false && $fallbackAnchor !== $anchor ) {
1902 $fallbackAnchor = htmlspecialchars( $fallbackAnchor, ENT_COMPAT );
1903 $fallback =
"<span id=\"$fallbackAnchor\"></span>";
1905 return "<h$level$attribs"
1906 .
"$fallback<span class=\"mw-headline\" id=\"$anchorEscaped\">$html</span>"
1918 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1920 if ( $trail !==
'' && preg_match( $regex, $trail, $m ) ) {
1921 [ , $inside, $trail ] = $m;
1923 return [ $inside, $trail ];
1961 $context ??= RequestContext::getMain();
1963 $editCount = self::getRollbackEditCount( $revRecord );
1964 if ( $editCount ===
false ) {
1968 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1970 $services = MediaWikiServices::getInstance();
1973 if ( !(
new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1974 $revRecord, $context, $options, $inner ) ) {
1978 if ( !in_array(
'noBrackets', $options,
true ) ) {
1979 $inner = $context->msg(
'brackets' )->rawParams( $inner )->escaped();
1982 if ( $services->getUserOptionsLookup()
1983 ->getBoolOption( $context->getUser(),
'showrollbackconfirmation' )
1985 $stats = $services->getStatsdDataFactory();
1986 $stats->increment(
'rollbackconfirmation.event.load' );
1987 $context->getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1990 return '<span class="mw-rollback-link">' . $inner .
'</span>';
2012 if ( func_num_args() > 1 ) {
2013 wfDeprecated( __METHOD__ .
' with $verify parameter',
'1.40' );
2015 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
2016 ->get( MainConfigNames::ShowRollbackEditCount );
2018 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
2023 $dbr = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getReplicaDatabase();
2026 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
2027 $res = $queryBuilder->where( [
'rev_page' => $revRecord->
getPageId() ] )
2028 ->useIndex( [
'revision' =>
'rev_page_timestamp' ] )
2029 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_DESC )
2030 ->limit( $showRollbackEditCount + 1 )
2031 ->caller( __METHOD__ )->fetchResultSet();
2033 $revUser = $revRecord->
getUser( RevisionRecord::RAW );
2034 $revUserText = $revUser ? $revUser->getName() :
'';
2038 foreach ( $res as $row ) {
2039 if ( $row->rev_user_text != $revUserText ) {
2040 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
2041 || $row->rev_deleted & RevisionRecord::DELETED_USER
2054 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
2082 $config = MediaWikiServices::getInstance()->getMainConfig();
2083 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
2084 $miserMode = $config->get( MainConfigNames::MiserMode );
2086 $disableRollbackEditCountSpecialPage = [
'Recentchanges',
'Watchlist' ];
2088 $context ??= RequestContext::getMain();
2091 $revUser = $revRecord->
getUser();
2092 $revUserText = $revUser ? $revUser->getName() :
'';
2095 'action' =>
'rollback',
2096 'from' => $revUserText,
2097 'token' => $context->getUser()->getEditToken(
'rollback' ),
2101 'data-mw' =>
'interface',
2102 'title' => $context->msg(
'tooltip-rollback' )->text()
2105 $options = [
'known',
'noclasses' ];
2107 if ( $context->getRequest()->getBool(
'bot' ) ) {
2109 $query[
'hidediff'] =
'1';
2110 $query[
'bot'] =
'1';
2114 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
2115 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
2116 $showRollbackEditCount =
false;
2123 $msg = [
'rollbacklink' ];
2124 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
2125 if ( !is_numeric( $editCount ) ) {
2126 $editCount = self::getRollbackEditCount( $revRecord );
2129 if ( $editCount > $showRollbackEditCount ) {
2130 $msg = [
'rollbacklinkcount-morethan',
Message::numParam( $showRollbackEditCount ) ];
2131 } elseif ( $editCount ) {
2136 $html = $context->msg( ...$msg )->parse();
2137 return self::link( $title, $html, $attrs, $query, $options );
2150 if ( count( $hiddencats ) > 0 ) {
2151 # Construct the HTML
2152 $outText =
'<div class="mw-hiddenCategoriesExplanation">';
2153 $outText .=
wfMessage(
'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
2154 $outText .=
"</div><ul>\n";
2156 foreach ( $hiddencats as $titleObj ) {
2157 # If it's hidden, it must exist - no need to check with a LinkBatch
2159 . self::link( $titleObj,
null, [], [],
'known' )
2162 $outText .=
'</ul>';
2170 private static function getContextFromMain() {
2171 $context = RequestContext::getMain();
2193 public static function titleAttrib( $name, $options =
null, array $msgParams = [], $localizer =
null ) {
2194 if ( !$localizer ) {
2195 $localizer = self::getContextFromMain();
2197 $message = $localizer->msg(
"tooltip-$name", $msgParams );
2200 if ( !$message->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
2201 $message = $localizer->msg(
'tooltip-ca-nstab' );
2204 if ( $message->isDisabled() ) {
2207 $tooltip = $message->text();
2208 # Compatibility: formerly some tooltips had [alt-.] hardcoded
2209 $tooltip = preg_replace(
"/ ?\[alt-.\]$/",
'', $tooltip );
2212 $options = (array)$options;
2214 if ( in_array(
'nonexisting', $options ) ) {
2215 $tooltip = $localizer->msg(
'red-link-title', $tooltip ?:
'' )->text();
2217 if ( in_array(
'withaccess', $options ) ) {
2218 $accesskey = self::accesskey( $name, $localizer );
2219 if ( $accesskey !==
false ) {
2221 if ( $tooltip ===
false || $tooltip ===
'' ) {
2222 $tooltip = $localizer->msg(
'brackets', $accesskey )->text();
2224 $tooltip .= $localizer->msg(
'word-separator' )->text();
2225 $tooltip .= $localizer->msg(
'brackets', $accesskey )->text();
2247 public static function accesskey( $name, $localizer =
null ) {
2248 if ( !isset( self::$accesskeycache[$name] ) ) {
2249 if ( !$localizer ) {
2250 $localizer = self::getContextFromMain();
2252 $msg = $localizer->msg(
"accesskey-$name" );
2255 if ( !$msg->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
2256 $msg = $localizer->msg(
'accesskey-ca-nstab' );
2258 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
2260 return self::$accesskeycache[$name];
2282 $canHide = $performer->
isAllowed(
'deleterevision' );
2283 $canHideHistory = $performer->
isAllowed(
'deletedhistory' );
2284 if ( !$canHide && !( $revRecord->
getVisibility() && $canHideHistory ) ) {
2288 if ( !$revRecord->
userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2289 return self::revDeleteLinkDisabled( $canHide );
2291 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2292 getPrefixedDBkey( $title );
2293 if ( $revRecord->
getId() ) {
2297 'type' =>
'revision',
2298 'target' => $prefixedDbKey,
2299 'ids' => $revRecord->
getId()
2305 'type' =>
'archive',
2306 'target' => $prefixedDbKey,
2310 return self::revDeleteLink(
2312 $revRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2329 public static function revDeleteLink( $query = [], $restricted =
false, $delete =
true ) {
2330 $sp = SpecialPage::getTitleFor(
'Revisiondelete' );
2331 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
2332 $html =
wfMessage( $msgKey )->escaped();
2333 $tag = $restricted ?
'strong' :
'span';
2334 $link = self::link( $sp, $html, [], $query, [
'known',
'noclasses' ] );
2337 [
'class' =>
'mw-revdelundel-link' ],
2338 wfMessage(
'parentheses' )->rawParams( $link )->escaped()
2354 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
2355 $html =
wfMessage( $msgKey )->escaped();
2356 $htmlParentheses =
wfMessage(
'parentheses' )->rawParams( $html )->escaped();
2357 return Xml::tags(
'span', [
'class' =>
'mw-revdelundel-link' ], $htmlParentheses );
2375 array $msgParams = [],
2379 $options = (array)$options;
2380 $options[] =
'withaccess';
2383 if ( !$localizer ) {
2384 $localizer = self::getContextFromMain();
2388 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2389 'accesskey' => self::accesskey( $name, $localizer )
2391 if ( $attribs[
'title'] ===
false ) {
2392 unset( $attribs[
'title'] );
2394 if ( $attribs[
'accesskey'] ===
false ) {
2395 unset( $attribs[
'accesskey'] );
2407 public static function tooltip( $name, $options =
null ) {
2408 $tooltip = self::titleAttrib( $name, $options );
2409 if ( $tooltip ===
false ) {
2412 return Xml::expandAttributes( [
2422class_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.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL using $wgServer (or one of its alternatives).
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') &&! $wgCommandLineMode $wgLang
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgTitle
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 ...
Implements some public methods and some protected utility functions which are required by multiple ch...
Marks HTML that shouldn't be escaped.
Base class for language-specific code.
A class containing constants representing the names of configuration variables.
const UploadNavigationUrl
Name constant for the UploadNavigationUrl setting, for use with Config::get()
const MaxTocLevel
Name constant for the MaxTocLevel 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.
The Message class deals with fetching and processing of interface message into a variety of formats.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
getTargetLanguage()
Get the target language for the content being parsed.
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
static getExternalLinkRel( $url=false, LinkTarget $title=null)
Get the rel attribute for a particular external link.
Group all the pieces relevant to the context of a request into one instance.
Module of static functions for generating XML.
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.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.