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();
403 $widthOption = $userOptionsLookup->getDefaultOption(
'thumbsize' );
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
1149 return $linkRenderer->makeExternalLink(
1151 $escape ? $text :
new HtmlArmor( $text ),
1173 $altUserName =
false,
1176 if ( $userName ===
'' || $userName ===
false || $userName ===
null ) {
1177 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1178 'that need to be fixed?' );
1179 return wfMessage(
'empty-username' )->parse();
1182 $classes =
'mw-userlink';
1184 $classes .=
' mw-tempuserlink';
1186 } elseif ( $userId == 0 ) {
1187 $page = ExternalUserNames::getUserLinkTitle( $userName );
1189 if ( ExternalUserNames::isExternal( $userName ) ) {
1190 $classes .=
' mw-extuserlink';
1191 } elseif ( $altUserName ===
false ) {
1192 $altUserName = IPUtils::prettifyIP( $userName );
1194 $classes .=
' mw-anonuserlink';
1196 $page = TitleValue::tryNew(
NS_USER, strtr( $userName,
' ',
'_' ) );
1201 '<bdi>' . htmlspecialchars( $altUserName !==
false ? $altUserName : $userName ) .
'</bdi>';
1203 if ( isset( $attributes[
'class'] ) ) {
1204 $attributes[
'class'] .=
' ' . $classes;
1206 $attributes[
'class'] = $classes;
1210 ?
self::link( $page, $linkText, $attributes )
1211 : Html::rawElement(
'span', $attributes, $linkText );
1233 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null
1237 $talkable = !( $disableAnonTalk && $userId == 0 );
1239 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1241 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1253 $attribs[
'class'] =
'mw-usertoollinks-contribs';
1254 if ( $redContribsWhenNoEdits ) {
1255 if ( $edits ===
null ) {
1256 $user = UserIdentityValue::newRegistered( $userId, $userText );
1257 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1259 if ( $edits === 0 ) {
1262 $attribs[
'class'] .=
' mw-usertoollinks-contribs-no-edits';
1269 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed(
'block' );
1270 if ( $blockable && $userCanBlock ) {
1277 ->newEmailUser( RequestContext::getMain()->
getAuthority() )
1284 (
new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1303 if ( $useParentheses ) {
1304 return wfMessage(
'word-separator' )->escaped()
1305 .
'<span class="mw-usertoollinks">'
1306 .
wfMessage(
'parentheses' )->rawParams(
$wgLang->pipeList( $items ) )->escaped()
1311 foreach ( $items as $tool ) {
1312 $tools[] = Html::rawElement(
'span', [], $tool );
1314 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1315 implode(
' ', $tools ) .
'</span>';
1333 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null,
1334 $useParentheses =
true
1336 if ( $userText ===
'' ) {
1337 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1338 'that need to be fixed?' );
1339 return ' ' .
wfMessage(
'empty-username' )->parse();
1342 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1343 return self::renderUserToolLinksArray( $items, $useParentheses );
1356 $userId, $userText, $edits =
null, $useParentheses =
true
1358 return self::userToolLinks( $userId, $userText,
true, 0, $edits, $useParentheses );
1368 if ( $userText ===
'' ) {
1369 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1370 'that need to be fixed?' );
1371 return wfMessage(
'empty-username' )->parse();
1374 $userTalkPage = TitleValue::tryNew(
NS_USER_TALK, strtr( $userText,
' ',
'_' ) );
1375 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-talk' ];
1376 $linkText =
wfMessage(
'talkpagelinktext' )->escaped();
1378 return $userTalkPage
1379 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1380 : Html::rawElement(
'span', $moreLinkAttribs, $linkText );
1390 if ( $userText ===
'' ) {
1391 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1392 'that need to be fixed?' );
1393 return wfMessage(
'empty-username' )->parse();
1396 $blockPage = SpecialPage::getTitleFor(
'Block', $userText );
1397 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-block' ];
1399 return self::link( $blockPage,
1411 if ( $userText ===
'' ) {
1412 wfLogWarning( __METHOD__ .
' received an empty username. Are there database errors ' .
1413 'that need to be fixed?' );
1414 return wfMessage(
'empty-username' )->parse();
1417 $emailPage = SpecialPage::getTitleFor(
'Emailuser', $userText );
1418 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-mail' ];
1419 return self::link( $emailPage,
1438 $authority = RequestContext::getMain()->getAuthority();
1440 $revUser = $revRecord->
getUser(
1441 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1445 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1448 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1451 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1452 $class = self::getRevisionDeletedClass( $revRecord );
1453 return '<span class="' . $class .
'">' . $link .
'</span>';
1465 $class =
'history-deleted';
1466 if ( $revisionRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1467 $class .=
' mw-history-suppressed';
1487 $useParentheses =
true
1490 $authority = RequestContext::getMain()->getAuthority();
1492 $revUser = $revRecord->
getUser(
1493 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1497 $link = self::userLink(
1499 $revUser->getName(),
1501 [
'data-mw-revid' => $revRecord->
getId() ]
1502 ) . self::userToolLinks(
1504 $revUser->getName(),
1512 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1515 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1516 $class = self::getRevisionDeletedClass( $revRecord );
1517 return ' <span class="' . $class .
' mw-userlink">' . $link .
'</span>';
1533 return HtmlHelper::modifyElements(
1535 static function ( SerializerNode $node ):
bool {
1536 return $node->name ===
'a' && isset( $node->attrs[
'href'] );
1538 static function ( SerializerNode $node ): SerializerNode {
1539 $node->attrs[
'href'] =
1555 # :Foobar -- override special treatment of prefix (images, language links)
1556 # /Foobar -- convert to CurrentPage/Foobar
1557 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1558 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1559 # ../Foobar -- convert to CurrentPage/Foobar,
1560 # (from CurrentPage/CurrentSubPage)
1561 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1562 # (from CurrentPage/CurrentSubPage)
1564 $ret = $target; #
default return value is no change
1566 # Some namespaces don't allow subpages,
1567 # so only perform processing if subpages are allowed
1569 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1570 hasSubpages( $contextTitle->getNamespace() )
1572 $hash = strpos( $target,
'#' );
1573 if ( $hash !==
false ) {
1574 $suffix = substr( $target, $hash );
1575 $target = substr( $target, 0, $hash );
1580 $target = trim( $target );
1581 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1582 getPrefixedText( $contextTitle );
1583 # Look at the first character
1584 if ( $target !=
'' && $target[0] ===
'/' ) {
1585 # / at end means we don't want the slash to be shown
1587 $trailingSlashes = preg_match_all(
'%(/+)$%', $target, $m );
1588 if ( $trailingSlashes ) {
1589 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1591 $noslash = substr( $target, 1 );
1594 $ret = $contextPrefixedText .
'/' . trim( $noslash ) . $suffix;
1595 if ( $text ===
'' ) {
1596 $text = $target . $suffix;
1597 } #
this might be changed
for ugliness reasons
1599 # check for .. subpage backlinks
1601 $nodotdot = $target;
1602 while ( str_starts_with( $nodotdot,
'../' ) ) {
1604 $nodotdot = substr( $nodotdot, 3 );
1606 if ( $dotdotcount > 0 ) {
1607 $exploded = explode(
'/', $contextPrefixedText );
1608 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1609 $ret = implode(
'/', array_slice( $exploded, 0, -$dotdotcount ) );
1610 # / at the end means don't show full path
1611 if ( substr( $nodotdot, -1, 1 ) ===
'/' ) {
1612 $nodotdot = rtrim( $nodotdot,
'/' );
1613 if ( $text ===
'' ) {
1614 $text = $nodotdot . $suffix;
1617 $nodotdot = trim( $nodotdot );
1618 if ( $nodotdot !=
'' ) {
1619 $ret .=
'/' . $nodotdot;
1637 $stxt =
wfMessage(
'historyempty' )->escaped();
1639 $stxt =
wfMessage(
'nbytes' )->numParams( $size )->escaped();
1641 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1651 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1653 if ( $trail !==
'' && preg_match( $regex, $trail, $m ) ) {
1654 [ , $inside, $trail ] = $m;
1656 return [ $inside, $trail ];
1694 $context ??= RequestContext::getMain();
1696 $editCount = self::getRollbackEditCount( $revRecord );
1697 if ( $editCount ===
false ) {
1701 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1703 $services = MediaWikiServices::getInstance();
1706 if ( !(
new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1707 $revRecord, $context, $options, $inner ) ) {
1711 if ( !in_array(
'noBrackets', $options,
true ) ) {
1712 $inner = $context->msg(
'brackets' )->rawParams( $inner )->escaped();
1715 if ( $services->getUserOptionsLookup()
1716 ->getBoolOption( $context->getUser(),
'showrollbackconfirmation' )
1718 $services->getStatsFactory()
1719 ->getCounter(
'rollbackconfirmation_event_load_total' )
1720 ->copyToStatsdAt(
'rollbackconfirmation.event.load' )
1722 $context->getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1725 return '<span class="mw-rollback-link">' . $inner .
'</span>';
1747 if ( func_num_args() > 1 ) {
1748 wfDeprecated( __METHOD__ .
' with $verify parameter',
'1.40' );
1750 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1751 ->get( MainConfigNames::ShowRollbackEditCount );
1753 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1758 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1761 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1762 $res = $queryBuilder->where( [
'rev_page' => $revRecord->
getPageId() ] )
1763 ->useIndex( [
'revision' =>
'rev_page_timestamp' ] )
1764 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_DESC )
1765 ->limit( $showRollbackEditCount + 1 )
1766 ->caller( __METHOD__ )->fetchResultSet();
1768 $revUser = $revRecord->
getUser( RevisionRecord::RAW );
1769 $revUserText = $revUser ? $revUser->getName() :
'';
1773 foreach ( $res as $row ) {
1774 if ( $row->rev_user_text != $revUserText ) {
1775 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1776 || $row->rev_deleted & RevisionRecord::DELETED_USER
1789 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1817 $config = MediaWikiServices::getInstance()->getMainConfig();
1818 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1819 $miserMode = $config->get( MainConfigNames::MiserMode );
1821 $disableRollbackEditCountSpecialPage = [
'Recentchanges',
'Watchlist' ];
1823 $context ??= RequestContext::getMain();
1826 $revUser = $revRecord->
getUser();
1827 $revUserText = $revUser ? $revUser->getName() :
'';
1830 'action' =>
'rollback',
1831 'from' => $revUserText,
1832 'token' => $context->getUser()->getEditToken(
'rollback' ),
1836 'data-mw' =>
'interface',
1837 'title' => $context->msg(
'tooltip-rollback' )->text()
1840 $options = [
'known',
'noclasses' ];
1842 if ( $context->getRequest()->getBool(
'bot' ) ) {
1844 $query[
'hidediff'] =
'1';
1845 $query[
'bot'] =
'1';
1849 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1850 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1851 $showRollbackEditCount =
false;
1858 $msg = [
'rollbacklink' ];
1859 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1860 if ( !is_numeric( $editCount ) ) {
1861 $editCount = self::getRollbackEditCount( $revRecord );
1864 if ( $editCount > $showRollbackEditCount ) {
1865 $msg = [
'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1866 } elseif ( $editCount ) {
1867 $msg = [
'rollbacklinkcount', Message::numParam( $editCount ) ];
1871 $html = $context->msg( ...$msg )->parse();
1872 return self::link( $title, $html, $attrs, $query, $options );
1885 if ( count( $hiddencats ) > 0 ) {
1886 # Construct the HTML
1887 $outText =
'<div class="mw-hiddenCategoriesExplanation">';
1888 $outText .=
wfMessage(
'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1889 $outText .=
"</div><ul>\n";
1891 foreach ( $hiddencats as $titleObj ) {
1892 # If it's hidden, it must exist - no need to check with a LinkBatch
1894 . self::link( $titleObj,
null, [], [],
'known' )
1897 $outText .=
'</ul>';
1905 private static function getContextFromMain() {
1906 $context = RequestContext::getMain();
1928 public static function titleAttrib( $name, $options =
null, array $msgParams = [], $localizer =
null ) {
1929 if ( !$localizer ) {
1930 $localizer = self::getContextFromMain();
1932 $message = $localizer->msg(
"tooltip-$name", $msgParams );
1935 if ( !$message->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
1936 $message = $localizer->msg(
'tooltip-ca-nstab' );
1939 if ( $message->isDisabled() ) {
1942 $tooltip = $message->text();
1943 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1944 $tooltip = preg_replace(
"/ ?\[alt-.\]$/",
'', $tooltip );
1947 $options = (array)$options;
1949 if ( in_array(
'nonexisting', $options ) ) {
1950 $tooltip = $localizer->msg(
'red-link-title', $tooltip ?:
'' )->text();
1952 if ( in_array(
'withaccess', $options ) ) {
1953 $accesskey = self::accesskey( $name, $localizer );
1954 if ( $accesskey !==
false ) {
1956 if ( $tooltip ===
false || $tooltip ===
'' ) {
1957 $tooltip = $localizer->msg(
'brackets', $accesskey )->text();
1959 $tooltip .= $localizer->msg(
'word-separator' )->text();
1960 $tooltip .= $localizer->msg(
'brackets', $accesskey )->text();
1982 public static function accesskey( $name, $localizer =
null ) {
1983 if ( !isset( self::$accesskeycache[$name] ) ) {
1984 if ( !$localizer ) {
1985 $localizer = self::getContextFromMain();
1987 $msg = $localizer->msg(
"accesskey-$name" );
1990 if ( !$msg->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
1991 $msg = $localizer->msg(
'accesskey-ca-nstab' );
1993 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1995 return self::$accesskeycache[$name];
2017 $canHide = $performer->
isAllowed(
'deleterevision' );
2018 $canHideHistory = $performer->
isAllowed(
'deletedhistory' );
2019 if ( !$canHide && !( $revRecord->
getVisibility() && $canHideHistory ) ) {
2023 if ( !$revRecord->
userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
2024 return self::revDeleteLinkDisabled( $canHide );
2026 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
2027 getPrefixedDBkey( $title );
2028 if ( $revRecord->
getId() ) {
2032 'type' =>
'revision',
2033 'target' => $prefixedDbKey,
2034 'ids' => $revRecord->
getId()
2040 'type' =>
'archive',
2041 'target' => $prefixedDbKey,
2045 return self::revDeleteLink(
2047 $revRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ),
2064 public static function revDeleteLink( $query = [], $restricted =
false, $delete =
true ) {
2065 $sp = SpecialPage::getTitleFor(
'Revisiondelete' );
2066 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
2067 $html =
wfMessage( $msgKey )->escaped();
2068 $tag = $restricted ?
'strong' :
'span';
2069 $link = self::link( $sp, $html, [], $query, [
'known',
'noclasses' ] );
2072 [
'class' =>
'mw-revdelundel-link' ],
2073 wfMessage(
'parentheses' )->rawParams( $link )->escaped()
2089 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
2090 $html =
wfMessage( $msgKey )->escaped();
2091 $htmlParentheses =
wfMessage(
'parentheses' )->rawParams( $html )->escaped();
2092 return Xml::tags(
'span', [
'class' =>
'mw-revdelundel-link' ], $htmlParentheses );
2110 array $msgParams = [],
2114 $options = (array)$options;
2115 $options[] =
'withaccess';
2118 if ( !$localizer ) {
2119 $localizer = self::getContextFromMain();
2123 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2124 'accesskey' => self::accesskey( $name, $localizer )
2126 if ( $attribs[
'title'] ===
false ) {
2127 unset( $attribs[
'title'] );
2129 if ( $attribs[
'accesskey'] ===
false ) {
2130 unset( $attribs[
'accesskey'] );
2142 public static function tooltip( $name, $options =
null ) {
2143 $tooltip = self::titleAttrib( $name, $options );
2144 if ( $tooltip ===
false ) {
2147 return Xml::expandAttributes( [
2155class_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') &&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.