33use Wikimedia\Assert\Assert;
35use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
37use Wikimedia\RemexHtml\Serializer\SerializerNode;
96 public static function link(
97 $target, $html =
null, $customAttribs = [], $query = [], $options = []
100 wfWarn( __METHOD__ .
': Requires $target to be a LinkTarget object.', 2 );
101 return "<!-- ERROR -->$html";
104 $options = (array)$options;
105 $linkRenderer = self::getLinkRenderer( $options );
107 if ( $html !==
null ) {
113 if ( in_array(
'known', $options,
true ) ) {
114 return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
117 if ( in_array(
'broken', $options,
true ) ) {
118 return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
121 if ( in_array(
'noclasses', $options,
true ) ) {
122 return $linkRenderer->makePreloadedLink( $target, $text,
'', $customAttribs, $query );
125 return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
148 $target, $html =
null, $customAttribs = [], $query = [], $options = [
'known' ]
151 return self::getLinkRenderer( $options )->makeKnownLink( $target, $html, $customAttribs, $query );
171 public static function makeSelfLinkObj( $nt, $html =
'', $query =
'', $trail =
'', $prefix =
'', $hash =
'' ) {
172 $nt = Title::newFromLinkTarget( $nt );
175 $attrs[
'class'] =
'mw-selflink-fragment';
176 $attrs[
'href'] =
'#' . $hash;
179 $attrs[
'class'] =
'mw-selflink selflink';
181 $ret = Html::rawElement(
'a', $attrs, $prefix . $html ) . $trail;
183 if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
188 $html = htmlspecialchars( $nt->getPrefixedText() );
191 return Html::rawElement(
'a', $attrs, $prefix . $html . $inside ) . $trail;
208 $name = $context->
msg(
'blanknamespace' )->text();
211 getFormattedNsText( $namespace );
213 return $context->
msg(
'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
216 return $context->
msg(
'invalidtitle-unknownnamespace', $namespace, $title )->text();
227 private static function fnamePart(
$url ) {
228 $basename = strrchr(
$url,
'/' );
229 if ( $basename ===
false ) {
232 $basename = substr( $basename, 1 );
249 $alt = self::fnamePart(
$url );
253 ->onLinkerMakeExternalImage(
$url, $alt, $img );
255 wfDebug(
"Hook LinkerMakeExternalImage changed the output of external image "
256 .
"with url {$url} and alt text {$alt} to {$img}" );
306 $file, $frameParams = [], $handlerParams = [], $time =
false,
307 $query =
'', $widthOption =
null
309 $title = Title::newFromLinkTarget( $title );
312 if ( !$hookRunner->onImageBeforeProduceHTML(
null, $title,
314 $file, $frameParams, $handlerParams, $time, $res,
316 $parser, $query, $widthOption )
321 if ( $file && !$file->allowInlineDisplay() ) {
322 wfDebug( __METHOD__ .
': ' . $title->getPrefixedDBkey() .
' does not allow inline display' );
327 $page = $handlerParams[
'page'] ??
false;
328 if ( !isset( $frameParams[
'align'] ) ) {
329 $frameParams[
'align'] =
'';
331 if ( !isset( $frameParams[
'title'] ) ) {
332 $frameParams[
'title'] =
'';
334 if ( !isset( $frameParams[
'class'] ) ) {
335 $frameParams[
'class'] =
'';
339 $config = $services->getMainConfig();
343 !isset( $handlerParams[
'width'] ) &&
344 !isset( $handlerParams[
'height'] ) &&
345 !isset( $frameParams[
'manualthumb'] ) &&
346 !isset( $frameParams[
'framed'] )
348 $classes[] =
'mw-default-size';
351 $prefix = $postfix =
'';
353 if ( $file && !isset( $handlerParams[
'width'] ) ) {
354 if ( isset( $handlerParams[
'height'] ) && $file->isVectorized() ) {
358 $handlerParams[
'width'] = $svgMaxSize;
360 $handlerParams[
'width'] = $file->getWidth( $page );
363 if ( isset( $frameParams[
'thumbnail'] )
364 || isset( $frameParams[
'manualthumb'] )
365 || isset( $frameParams[
'framed'] )
366 || isset( $frameParams[
'frameless'] )
367 || !$handlerParams[
'width']
371 if ( $widthOption ===
null || !isset( $thumbLimits[$widthOption] ) ) {
372 $userOptionsLookup = $services->getUserOptionsLookup();
377 if ( isset( $frameParams[
'upright'] ) && $frameParams[
'upright'] == 0 ) {
378 $frameParams[
'upright'] = $thumbUpright;
384 $prefWidth = isset( $frameParams[
'upright'] ) ?
385 round( $thumbLimits[$widthOption] * $frameParams[
'upright'], -1 ) :
386 $thumbLimits[$widthOption];
390 if ( !isset( $handlerParams[
'height'] ) && ( $handlerParams[
'width'] <= 0 ||
391 $prefWidth < $handlerParams[
'width'] || $file->isVectorized() ) ) {
392 $handlerParams[
'width'] = $prefWidth;
398 $hasVisibleCaption = isset( $frameParams[
'thumbnail'] ) ||
399 isset( $frameParams[
'manualthumb'] ) ||
400 isset( $frameParams[
'framed'] );
402 if ( $hasVisibleCaption ) {
404 $title, $file, $frameParams, $handlerParams, $time, $query,
409 $rdfaType =
'mw:File';
411 if ( isset( $frameParams[
'frameless'] ) ) {
412 $rdfaType .=
'/Frameless';
414 $srcWidth = $file->getWidth( $page );
415 # For "frameless" option: do not present an image bigger than the
416 # source (for bitmap-style images). This is the same behavior as the
417 # "thumb" option does it already.
418 if ( $srcWidth && !$file->mustRender() && $handlerParams[
'width'] > $srcWidth ) {
419 $handlerParams[
'width'] = $srcWidth;
424 if ( $file && isset( $handlerParams[
'width'] ) ) {
425 # Create a resized image, without the additional thumbnail features
426 $thumb = $file->transform( $handlerParams );
431 $isBadFile = $file && $thumb &&
434 if ( !$thumb || $thumb->isError() || $isBadFile ) {
435 $rdfaType =
'mw:Error ' . $rdfaType;
436 $currentExists = $file && $file->exists();
437 if ( $currentExists && !$thumb ) {
438 $label =
wfMessage(
'thumbnail_error',
'' )->text();
439 } elseif ( $thumb && $thumb->isError() ) {
442 'Unknown MediaTransformOutput: ' . get_class( $thumb )
444 $label = $thumb->toText();
446 $label = $frameParams[
'alt'] ??
'';
449 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams, $currentExists
457 if ( isset( $frameParams[
'alt'] ) ) {
458 $params[
'alt'] = $frameParams[
'alt'];
460 $params[
'title'] = $frameParams[
'title'];
462 'img-class' =>
'mw-file-element',
465 isset( $frameParams[
'upright'] ) &&
466 in_array(
'mw-default-size', $classes,
true ) &&
467 isset( $frameParams[
'frameless'] )
469 $params[
'img-class'] .=
' mw-file-upright';
470 $params[
'style'] =
'--mw-file-upright: ' . $frameParams[
'upright'];
473 $s = $thumb->toHtml( $params );
479 if ( $frameParams[
'align'] !=
'' ) {
482 $classes[] =
"mw-halign-{$frameParams['align']}";
483 $caption = Html::rawElement(
484 'figcaption', [], $frameParams[
'caption'] ??
''
486 } elseif ( isset( $frameParams[
'valign'] ) ) {
490 $classes[] =
"mw-valign-{$frameParams['valign']}";
493 if ( isset( $frameParams[
'border'] ) ) {
494 $classes[] =
'mw-image-border';
497 if ( isset( $frameParams[
'class'] ) ) {
498 $classes[] = $frameParams[
'class'];
503 'typeof' => $rdfaType,
506 $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
508 return str_replace(
"\n",
' ', $s );
521 if ( isset( $frameParams[
'link-url'] ) && $frameParams[
'link-url'] !==
'' ) {
522 $mtoParams[
'custom-url-link'] = $frameParams[
'link-url'];
523 if ( isset( $frameParams[
'link-target'] ) ) {
524 $mtoParams[
'custom-target-link'] = $frameParams[
'link-target'];
527 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams[
'link-url'] );
528 foreach ( $extLinkAttrs as $name => $val ) {
530 $mtoParams[
'parser-extlink-' . $name] = $val;
533 } elseif ( isset( $frameParams[
'link-title'] ) && $frameParams[
'link-title'] !==
'' ) {
534 $mtoParams[
'custom-title-link'] = Title::newFromLinkTarget(
535 self::getLinkRenderer()->normalizeTarget( $frameParams[
'link-title'] )
537 if ( isset( $frameParams[
'link-title-query'] ) ) {
538 $mtoParams[
'custom-title-link-query'] = $frameParams[
'link-title-query'];
540 } elseif ( !empty( $frameParams[
'no-link'] ) ) {
543 $mtoParams[
'desc-link'] =
true;
544 $mtoParams[
'desc-query'] = $query;
562 LinkTarget $title, $file, $label =
'', $alt =
'', $align =
null,
563 $params = [], $framed =
false, $manualthumb =
''
571 if ( $manualthumb ) {
572 $frameParams[
'manualthumb'] = $manualthumb;
573 } elseif ( $framed ) {
574 $frameParams[
'framed'] =
true;
575 } elseif ( !isset( $params[
'width'] ) && !isset( $params[
'height'] ) ) {
576 $classes[] =
'mw-default-size';
579 $title, $file, $frameParams, $params,
false,
'', $classes
595 LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
596 $time =
false, $query =
'', array $classes = [], ?
Parser $parser =
null
598 $exists = $file && $file->exists();
601 $page = $handlerParams[
'page'] ??
false;
602 $lang = $handlerParams[
'lang'] ??
false;
604 if ( !isset( $frameParams[
'align'] ) ) {
605 $frameParams[
'align'] =
'';
607 if ( !isset( $frameParams[
'caption'] ) ) {
608 $frameParams[
'caption'] =
'';
611 if ( empty( $handlerParams[
'width'] ) ) {
613 $handlerParams[
'width'] = isset( $frameParams[
'upright'] ) ? 130 : 180;
618 $manualthumb =
false;
620 $rdfaType =
'mw:File/Thumb';
624 if ( !isset( $frameParams[
'manualthumb'] ) && isset( $frameParams[
'framed'] ) ) {
625 $rdfaType =
'mw:File/Frame';
627 $outerWidth = $handlerParams[
'width'] + 2;
629 if ( isset( $frameParams[
'manualthumb'] ) ) {
630 # Use manually specified thumbnail
631 $manual_title = Title::makeTitleSafe(
NS_FILE, $frameParams[
'manualthumb'] );
632 if ( $manual_title ) {
633 $manual_img = $services->getRepoGroup()
634 ->findFile( $manual_title );
636 $thumb = $manual_img->getUnscaledThumb( $handlerParams );
641 $srcWidth = $file->getWidth( $page );
642 if ( isset( $frameParams[
'framed'] ) ) {
643 $rdfaType =
'mw:File/Frame';
644 if ( !$file->isVectorized() ) {
650 $handlerParams[
'width'] = $srcWidth;
656 if ( $srcWidth && !$file->mustRender() && $handlerParams[
'width'] > $srcWidth ) {
657 $handlerParams[
'width'] = $srcWidth;
661 ? $file->getUnscaledThumb( $handlerParams )
662 : $file->transform( $handlerParams );
666 $outerWidth = $thumb->getWidth() + 2;
668 $outerWidth = $handlerParams[
'width'] + 2;
672 if ( $parser && $rdfaType ===
'mw:File/Thumb' ) {
673 $parser->getOutput()->addModules( [
'mediawiki.page.media' ] );
676 $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
677 $linkTitleQuery = [];
678 if ( $page || $lang ) {
680 $linkTitleQuery[
'page'] = $page;
683 $linkTitleQuery[
'lang'] = $lang;
685 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
686 # So we don't need to pass it here in $query. However, the URL for the
687 # zoom icon still needs it, so we make a unique query for it. See T16771
692 && !isset( $frameParams[
'link-title'] )
693 && !isset( $frameParams[
'link-url'] )
694 && !isset( $frameParams[
'no-link'] ) ) {
695 $frameParams[
'link-title'] = $title;
696 $frameParams[
'link-title-query'] = $linkTitleQuery;
699 if ( $frameParams[
'align'] !=
'' ) {
701 $classes[] =
"mw-halign-{$frameParams['align']}";
704 if ( isset( $frameParams[
'class'] ) ) {
705 $classes[] = $frameParams[
'class'];
710 $isBadFile = $exists && $thumb && $parser &&
711 $parser->getBadFileLookup()->isBadFile(
712 $manualthumb ? $manual_title->getDBkey() : $title->
getDBkey(),
717 $rdfaType =
'mw:Error ' . $rdfaType;
718 $label = $frameParams[
'alt'] ??
'';
720 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams,
false
723 } elseif ( !$thumb || $thumb->isError() || $isBadFile ) {
724 $rdfaType =
'mw:Error ' . $rdfaType;
725 if ( $thumb && $thumb->isError() ) {
728 'Unknown MediaTransformOutput: ' . get_class( $thumb )
730 $label = $thumb->toText();
731 } elseif ( !$thumb ) {
732 $label =
wfMessage(
'thumbnail_error',
'' )->text();
737 $title, $label,
'',
'',
'', (
bool)$time, $handlerParams,
true
741 if ( !$noscale && !$manualthumb ) {
748 if ( isset( $frameParams[
'alt'] ) ) {
749 $params[
'alt'] = $frameParams[
'alt'];
752 'img-class' =>
'mw-file-element',
755 isset( $frameParams[
'upright'] ) &&
756 in_array(
'mw-default-size', $classes,
true )
758 $params[
'img-class'] .=
' mw-file-upright';
759 $params[
'style'] =
'--mw-file-upright: ' . $frameParams[
'upright'];
762 if ( $rdfaType ===
'mw:File/Thumb' ) {
763 $params[
'magnify-resource'] =
$url;
766 $s .= $thumb->toHtml( $params );
767 if ( isset( $frameParams[
'framed'] ) ) {
770 $zoomIcon = Html::rawElement(
'div', [
'class' =>
'magnify' ],
771 Html::rawElement(
'a', [
773 'class' =>
'internal',
774 'title' =>
wfMessage(
'thumbnail-more' )->text(),
780 $s .= Html::rawElement(
781 'figcaption', [], $frameParams[
'caption'] ??
''
786 'typeof' => $rdfaType,
789 $s = Html::rawElement(
'figure', $attribs, $s );
791 return str_replace(
"\n",
' ', $s );
804 if ( $responsiveImages && $thumb && !$thumb->isError() ) {
806 $hp20[
'width'] = $hp[
'width'] * 2;
807 if ( isset( $hp[
'height'] ) ) {
808 $hp20[
'height'] = $hp[
'height'] * 2;
810 $thumb20 = $file->transform( $hp20 );
811 if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
812 $thumb->responsiveUrls[
'2'] = $thumb20->getUrl();
835 $title, $label =
'', $query =
'', $unused1 =
'', $unused2 =
'',
836 $time =
false, array $handlerParams = [],
bool $currentExists =
false
838 if ( !$title instanceof ParsoidLinkTarget ) {
839 wfDeprecatedMsg( __METHOD__ .
': Requires $title to be a LinkTarget object',
'1.47' );
840 return "<!-- ERROR -->" . htmlspecialchars( $label );
842 if ( $query !==
'' ) {
843 wfDeprecated( __METHOD__ .
' with non-empty query parameter',
'1.47' );
846 $title = Title::newFromLinkTarget( $title );
848 $mainConfig = $services->getMainConfig();
852 if ( $label ==
'' ) {
853 $label = $title->getPrefixedText();
857 'class' =>
'mw-file-element mw-broken-media',
859 'data-width' => $handlerParams[
'width'] ??
null,
860 'data-height' => $handlerParams[
'height'] ??
null,
863 $repoGroup = $services->getRepoGroup();
864 $currentExists = $currentExists ||
865 ( $time && $repoGroup->findFile( $title ) !== false );
867 if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
871 $title->inNamespace(
NS_FILE ) &&
872 $repoGroup->getLocalRepo()->checkRedirect( $title )
878 [
'class' =>
'mw-redirect' ],
880 [
'known',
'noclasses' ]
883 return Html::rawElement(
'a', [
884 'href' => self::getUploadUrl( $title, $query ),
886 'title' => $title->getPrefixedText()
894 [
'known',
'noclasses' ]
909 public static function getUploadUrl( ParsoidLinkTarget $destFile,
string $query =
'',
bool $prefixedURL =
false ) {
913 $q =
'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
914 if ( $query !=
'' ) {
915 wfDeprecated( __METHOD__ .
' with $query parameter',
'1.47' );
919 if ( $uploadMissingFileUrl ) {
923 if ( $uploadNavigationUrl ) {
929 if ( $prefixedURL ) {
930 return './' . $upload->getPrefixedURL( $q );
932 return $upload->getLocalURL( $q );
946 $title, [
'time' => $time ]
964 if ( $file && $file->exists() ) {
965 $url = $file->getUrl();
985 Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
987 wfDebug(
"Hook LinkerMakeMediaLinkFile changed the output of link "
988 .
"with url {$url} and text {$html} to {$ret}" );
992 return Html::rawElement(
'a', $attribs, $html );
1006 $queryPos = strpos( $name,
'?' );
1007 if ( $queryPos !==
false ) {
1008 $getParams =
wfCgiToArray( substr( $name, $queryPos + 1 ) );
1009 $name = substr( $name, 0, $queryPos );
1014 $slashPos = strpos( $name,
'/' );
1015 if ( $slashPos !==
false ) {
1016 $subpage = substr( $name, $slashPos + 1 );
1017 $name = substr( $name, 0, $slashPos );
1023 $key = strtolower( $name );
1026 return self::getLinkRenderer()->makeKnownLink(
1055 $linktype =
'', $attribs = [], $title =
null
1059 return self::getLinkRenderer()->makeExternalLink(
1061 $escape ? $text :
new HtmlArmor( $text ),
1085 $altUserName =
false,
1088 if ( $userName ===
'' || $userName ===
false || $userName ===
null ) {
1089 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1090 'that need to be fixed?' );
1091 return wfMessage(
'empty-username' )->parse();
1094 return self::getLinkRenderer()->makeUserLink(
1096 RequestContext::getMain(),
1097 $altUserName ===
false ?
null : (string)$altUserName,
1121 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null
1125 $talkable = !( $disableAnonTalk && $userId == 0 );
1127 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1129 if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1142 if ( $userId && !$services->getTempUserConfig()->isTempName( $userText ) ) {
1144 $attribs[
'class'] =
'mw-usertoollinks-contribs';
1147 if ( $redContribsWhenNoEdits ) {
1148 if ( $edits ===
null ) {
1149 $user = UserIdentityValue::newRegistered( $userId, $userText );
1150 $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1152 if ( $edits === 0 ) {
1155 $attribs[
'class'] .=
' mw-usertoollinks-contribs-no-edits';
1162 $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed(
'block' );
1163 if ( $blockable && $userCanBlock ) {
1170 ->newEmailUser( RequestContext::getMain()->
getAuthority() )
1177 (
new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1194 if ( $useParentheses ) {
1195 $lang = RequestContext::getMain()->getLanguage();
1196 return wfMessage(
'word-separator' )->escaped()
1197 .
'<span class="mw-usertoollinks">'
1198 .
wfMessage(
'parentheses' )->rawParams( $lang->pipeList( $items ) )->escaped()
1203 foreach ( $items as $tool ) {
1204 $tools[] = Html::rawElement(
'span', [], $tool );
1206 return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1207 implode(
' ', $tools ) .
'</span>';
1225 $userId, $userText, $redContribsWhenNoEdits =
false, $flags = 0, $edits =
null,
1226 $useParentheses =
true
1228 if ( $userText ===
'' ) {
1229 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1230 'that need to be fixed?' );
1231 return ' ' .
wfMessage(
'empty-username' )->parse();
1234 $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1235 return self::renderUserToolLinksArray( $items, $useParentheses );
1248 $userId, $userText, $edits =
null, $useParentheses =
true
1250 return self::userToolLinks( $userId, $userText,
true, 0, $edits, $useParentheses );
1260 if ( $userText ===
'' ) {
1261 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1262 'that need to be fixed?' );
1263 return wfMessage(
'empty-username' )->parse();
1266 $userTalkPage = TitleValue::tryNew(
NS_USER_TALK, strtr( $userText,
' ',
'_' ) );
1267 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-talk' ];
1268 $linkText =
wfMessage(
'talkpagelinktext' )->escaped();
1270 return $userTalkPage
1271 ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1272 : Html::rawElement(
'span', $moreLinkAttribs, $linkText );
1282 if ( $userText ===
'' ) {
1283 wfDebug( __METHOD__ .
' received an empty username. Are there database errors ' .
1284 'that need to be fixed?' );
1285 return wfMessage(
'empty-username' )->parse();
1288 $blockPage = SpecialPage::getTitleFor(
'Block', $userText );
1289 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-block' ];
1291 return self::link( $blockPage,
1303 if ( $userText ===
'' ) {
1304 wfLogWarning( __METHOD__ .
' received an empty username. Are there database errors ' .
1305 'that need to be fixed?' );
1306 return wfMessage(
'empty-username' )->parse();
1309 $emailPage = SpecialPage::getTitleFor(
'Emailuser', $userText );
1310 $moreLinkAttribs = [
'class' =>
'mw-usertoollinks-mail' ];
1311 return self::link( $emailPage,
1330 $authority = RequestContext::getMain()->getAuthority();
1332 $revUser = $revRecord->
getUser(
1333 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1337 $link = self::userLink( $revUser->getId(), $revUser->getName() );
1340 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1343 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1344 $class = self::getRevisionDeletedClass( $revRecord );
1345 return '<span class="' . $class .
'">' . $link .
'</span>';
1357 $class =
'history-deleted';
1358 if ( $revisionRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1359 $class .=
' mw-history-suppressed';
1379 $useParentheses =
true
1382 $authority = RequestContext::getMain()->getAuthority();
1384 $revUser = $revRecord->
getUser(
1385 $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1389 $link = self::userLink(
1391 $revUser->getName(),
1393 [
'data-mw-revid' => $revRecord->
getId() ]
1394 ) . self::userToolLinks(
1396 $revUser->getName(),
1404 $link =
wfMessage(
'rev-deleted-user' )->escaped();
1407 if ( $revRecord->
isDeleted( RevisionRecord::DELETED_USER ) ) {
1408 $class = self::getRevisionDeletedClass( $revRecord );
1409 return ' <span class="' . $class .
' mw-userlink">' . $link .
'</span>';
1425 return HtmlHelper::modifyElements(
1427 static function ( SerializerNode $node ):
bool {
1428 return $node->name ===
'a' && isset( $node->attrs[
'href'] );
1430 static function ( SerializerNode $node ): SerializerNode {
1431 $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1432 $href = $urlUtils->expand( $node->attrs[
'href'],
PROTO_RELATIVE );
1433 if ( $href !==
null ) {
1434 $node->attrs[
'href'] = $href;
1450 # :Foobar -- override special treatment of prefix (images, language links)
1451 # /Foobar -- convert to CurrentPage/Foobar
1452 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1453 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1454 # ../Foobar -- convert to CurrentPage/Foobar,
1455 # (from CurrentPage/CurrentSubPage)
1456 # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1457 # (from CurrentPage/CurrentSubPage)
1459 $ret = $target; #
default return value is no change
1461 # Some namespaces don't allow subpages,
1462 # so only perform processing if subpages are allowed
1464 $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1465 hasSubpages( $contextTitle->getNamespace() )
1467 $hash = strpos( $target,
'#' );
1468 if ( $hash !==
false ) {
1469 $suffix = substr( $target, $hash );
1470 $target = substr( $target, 0, $hash );
1475 $target = trim( $target );
1476 $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1477 getPrefixedText( $contextTitle );
1478 # Look at the first character
1479 if ( $target !=
'' && $target[0] ===
'/' ) {
1480 # / at end means we don't want the slash to be shown
1482 $trailingSlashes = preg_match_all(
'%(/+)$%', $target, $m );
1483 if ( $trailingSlashes ) {
1484 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1486 $noslash = substr( $target, 1 );
1489 $ret = $contextPrefixedText .
'/' . trim( $noslash ) . $suffix;
1490 if ( $text ===
'' ) {
1491 $text = $target . $suffix;
1492 } #
this might be changed
for ugliness reasons
1494 # check for .. subpage backlinks
1496 $nodotdot = $target;
1497 while ( str_starts_with( $nodotdot,
'../' ) ) {
1499 $nodotdot = substr( $nodotdot, 3 );
1501 if ( $dotdotcount > 0 ) {
1502 $exploded = explode(
'/', $contextPrefixedText );
1503 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1504 $ret = implode(
'/', array_slice( $exploded, 0, -$dotdotcount ) );
1505 # / at the end means don't show full path
1506 if ( substr( $nodotdot, -1, 1 ) ===
'/' ) {
1507 $nodotdot = rtrim( $nodotdot,
'/' );
1508 if ( $text ===
'' ) {
1509 $text = $nodotdot . $suffix;
1512 $nodotdot = trim( $nodotdot );
1513 if ( $nodotdot !=
'' ) {
1514 $ret .=
'/' . $nodotdot;
1532 $stxt =
wfMessage(
'historyempty' )->escaped();
1534 $stxt =
wfMessage(
'nbytes' )->numParams( $size )->escaped();
1536 return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1546 $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1548 if ( $trail !==
'' && preg_match( $regex, $trail, $m ) ) {
1549 [ , $inside, $trail ] = $m;
1551 return [ $inside, $trail ];
1589 $context ??= RequestContext::getMain();
1591 $editCount = self::getRollbackEditCount( $revRecord );
1592 if ( $editCount ===
false ) {
1596 $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1598 $services = MediaWikiServices::getInstance();
1601 if ( !(
new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1602 $revRecord, $context, $options, $inner ) ) {
1606 if ( !in_array(
'noBrackets', $options,
true ) ) {
1607 $inner = $context->msg(
'brackets' )->rawParams( $inner )->escaped();
1610 if ( $services->getUserOptionsLookup()
1611 ->getBoolOption( $context->getUser(),
'showrollbackconfirmation' )
1613 $context->getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1616 return '<span class="mw-rollback-link">' . $inner .
'</span>';
1638 if ( func_num_args() > 1 ) {
1639 wfDeprecated( __METHOD__ .
' with $verify parameter',
'1.40' );
1641 $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1642 ->get( MainConfigNames::ShowRollbackEditCount );
1644 if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1649 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1652 $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1653 $res = $queryBuilder->where( [
'rev_page' => $revRecord->
getPageId() ] )
1654 ->useIndex( [
'revision' =>
'rev_page_timestamp' ] )
1655 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_DESC )
1656 ->limit( $showRollbackEditCount + 1 )
1657 ->caller( __METHOD__ )->fetchResultSet();
1659 $revUser = $revRecord->
getUser( RevisionRecord::RAW );
1660 $revUserText = $revUser ? $revUser->getName() :
'';
1664 foreach ( $res as $row ) {
1665 if ( $row->rev_user_text != $revUserText ) {
1666 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1667 || $row->rev_deleted & RevisionRecord::DELETED_USER
1680 if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1708 $config = MediaWikiServices::getInstance()->getMainConfig();
1709 $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1710 $miserMode = $config->get( MainConfigNames::MiserMode );
1712 $disableRollbackEditCountSpecialPage = [
'Recentchanges',
'Watchlist' ];
1714 $context ??= RequestContext::getMain();
1717 $revUser = $revRecord->
getUser();
1718 $revUserText = $revUser ? $revUser->getName() :
'';
1721 'action' =>
'rollback',
1722 'from' => $revUserText,
1723 'token' => $context->getUser()->getEditToken(
'rollback' ),
1727 'data-mw-interface' =>
'',
1728 'title' => $context->msg(
'tooltip-rollback' )->text()
1731 $options = [
'known',
'noclasses' ];
1733 if ( $context->getRequest()->getBool(
'bot' ) ) {
1735 $query[
'hidediff'] =
'1';
1736 $query[
'bot'] =
'1';
1740 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1741 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1742 $showRollbackEditCount =
false;
1749 $msg = [
'rollbacklink' ];
1750 if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1751 if ( !is_numeric( $editCount ) ) {
1752 $editCount = self::getRollbackEditCount( $revRecord );
1755 if ( $editCount > $showRollbackEditCount ) {
1756 $msg = [
'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1757 } elseif ( $editCount ) {
1758 $msg = [
'rollbacklinkcount', Message::numParam( $editCount ) ];
1762 $html = $context->msg( ...$msg )->parse();
1763 return self::link( $title, $html, $attrs, $query, $options );
1776 if ( count( $hiddencats ) > 0 ) {
1777 # Construct the HTML
1778 $outText =
'<div class="mw-hiddenCategoriesExplanation">';
1779 $outText .=
wfMessage(
'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1780 $outText .=
"</div><ul>\n";
1782 foreach ( $hiddencats as $titleObj ) {
1783 # If it's hidden, it must exist - no need to check with a LinkBatch
1785 . self::link( $titleObj,
null, [], [],
'known' )
1788 $outText .=
'</ul>';
1796 private static function getContextFromMain() {
1797 $context = RequestContext::getMain();
1820 public static function titleAttrib( $name, $options =
null, array $msgParams = [], $localizer =
null ) {
1821 if ( !$localizer ) {
1822 $localizer = self::getContextFromMain();
1824 $message = $localizer->msg(
"tooltip-$name", $msgParams );
1827 if ( !$message->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
1828 $message = $localizer->msg(
'tooltip-ca-nstab' );
1831 if ( $message->isDisabled() ) {
1834 $tooltip = $message->text();
1835 # Compatibility: formerly some tooltips had [alt-.] hardcoded
1836 $tooltip = preg_replace(
"/ ?\[alt-.\]$/",
'', $tooltip );
1839 $options = (array)$options;
1841 if ( in_array(
'nonexisting', $options ) ) {
1842 $tooltip = $localizer->msg(
'red-link-title', $tooltip ?:
'' )->text();
1844 if ( in_array(
'withaccess', $options ) ) {
1845 $accesskey = self::accesskey( $name, $localizer );
1846 if ( $accesskey !==
false ) {
1848 if ( $tooltip ===
false || $tooltip ===
'' ) {
1849 $tooltip = $localizer->msg(
'brackets', $accesskey )->text();
1851 $tooltip .= $localizer->msg(
'word-separator' )->text();
1852 $tooltip .= $localizer->msg(
'brackets', $accesskey )->text();
1878 public static function accesskey( $name, $localizer =
null ) {
1884 if ( !isset( self::$accesskeycache[$name] ) ) {
1885 $localizer ??= RequestContext::getMain();
1886 $msg = $localizer->msg(
"accesskey-$name" );
1889 if ( !$msg->exists() && str_starts_with( $name,
'ca-nstab-' ) ) {
1890 $msg = $localizer->msg(
'accesskey-ca-nstab' );
1892 self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1894 return self::$accesskeycache[$name];
1916 $canHide = $performer->
isAllowed(
'deleterevision' );
1917 $canHideHistory = $performer->
isAllowed(
'deletedhistory' );
1918 if ( !$canHide && !( $revRecord->
getVisibility() && $canHideHistory ) ) {
1922 if ( !$revRecord->
userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
1923 return self::revDeleteLinkDisabled( $canHide );
1925 $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
1926 getPrefixedDBkey( $title );
1927 if ( $revRecord->
getId() ) {
1931 'type' =>
'revision',
1932 'target' => $prefixedDbKey,
1933 'ids' => $revRecord->
getId()
1939 'type' =>
'archive',
1940 'target' => $prefixedDbKey,
1944 return self::revDeleteLink(
1946 $revRecord->
isDeleted( RevisionRecord::DELETED_RESTRICTED ),
1963 public static function revDeleteLink( $query = [], $restricted =
false, $delete =
true ) {
1964 $sp = SpecialPage::getTitleFor(
'Revisiondelete' );
1965 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
1966 $html =
wfMessage( $msgKey )->escaped();
1967 $tag = $restricted ?
'strong' :
'span';
1968 $link = self::link( $sp, $html, [], $query, [
'known',
'noclasses' ] );
1969 return Html::rawElement(
1971 [
'class' =>
'mw-revdelundel-link' ],
1972 wfMessage(
'parentheses' )->rawParams( $link )->escaped()
1988 $msgKey = $delete ?
'rev-delundel' :
'rev-showdeleted';
1989 $html =
wfMessage( $msgKey )->escaped();
1990 $htmlParentheses =
wfMessage(
'parentheses' )->rawParams( $html )->escaped();
1991 return Html::rawElement(
'span', [
'class' =>
'mw-revdelundel-link' ], $htmlParentheses );
2009 array $msgParams = [],
2013 $options = (array)$options;
2014 $options[] =
'withaccess';
2017 if ( !$localizer ) {
2018 $localizer = self::getContextFromMain();
2022 'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
2023 'accesskey' => self::accesskey( $name, $localizer )
2025 if ( $attribs[
'title'] ===
false ) {
2026 unset( $attribs[
'title'] );
2028 if ( $attribs[
'accesskey'] ===
false ) {
2029 unset( $attribs[
'accesskey'] );
2041 public static function tooltip( $name, $options =
null ) {
2042 $tooltip = self::titleAttrib( $name, $options );
2043 if ( $tooltip ===
false ) {
2046 return Html::expandAttributes( [
2055 private static function getLinkRenderer(
2056 array $legacyOptions = []
2060 if ( count( $legacyOptions ) > 0 ) {
2066 return $services->getLinkRenderer();
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.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli') global $wgTitle
if(!defined('MW_SETUP_CALLBACK'))
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 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,...
Interface for objects which can provide a MediaWiki context on request.