64 return new $classname( $this->
getConfig() );
74 throw new RuntimeException(
75 'SkinTemplate skins must define a `template` either as a public'
76 .
' property of by passing in a`template` option to the constructor.'
97 $this->thispage =
$title->getPrefixedDBkey();
98 $this->titletxt =
$title->getPrefixedText();
99 $this->userpage = $user->getUserPage()->getPrefixedText();
101 if ( !$request->wasPosted() ) {
102 $query = $request->getValues();
103 unset( $query[
'title'] );
104 unset( $query[
'returnto'] );
105 unset( $query[
'returntoquery'] );
108 $this->loggedin = $user->isRegistered();
109 $this->username = $user->getName();
111 if ( $this->loggedin ) {
114 # This won't be used in the standard skins, but we define it to preserve the interface
115 # To save time, we check for existence
131 return $tpl->execute();
168 # An ID that includes the actual body text; without categories, contentSub, ...
169 $realBodyAttribs = [
'id' =>
'mw-content-text' ];
171 # Add a mw-content-ltr/rtl class to be able to style based on text
172 # direction when the content is different from the UI language (only
174 # Most information on special pages and file pages is in user language,
175 # rather than content language, so those will not get this
178 $pageLang =
$title->getPageViewLanguage();
179 $realBodyAttribs[
'lang'] = $pageLang->getHtmlCode();
180 $realBodyAttribs[
'dir'] = $pageLang->getDir();
181 $realBodyAttribs[
'class'] =
'mw-content-' . $pageLang->getDir();
194 $userLangCode = $userLang->getHtmlCode();
195 $userLangDir = $userLang->getDir();
196 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
198 $userLangCode !== $contLang->getHtmlCode() ||
199 $userLangDir !== $contLang->getDir()
201 $escUserlang = htmlspecialchars( $userLangCode );
202 $escUserdir = htmlspecialchars( $userLangDir );
205 return " lang=\"$escUserlang\" dir=\"$escUserdir\"";
219 foreach ( $config->get(
'FooterIcons' ) as $footerIconsKey => &$footerIconsBlock ) {
220 if ( count( $footerIconsBlock ) > 0 ) {
221 $footericons[$footerIconsKey] = [];
222 foreach ( $footerIconsBlock as &$footerIcon ) {
223 if ( isset( $footerIcon[
'src'] ) ) {
224 if ( !isset( $footerIcon[
'width'] ) ) {
225 $footerIcon[
'width'] = 88;
227 if ( !isset( $footerIcon[
'height'] ) ) {
228 $footerIcon[
'height'] = 31;
237 if ( is_string( $footerIcon ) || isset( $footerIcon[
'src'] ) ) {
238 $footericons[$footerIconsKey][] = $footerIcon;
253 return $undelete ===
'' ? null :
'<span class="subpages">' . $undelete .
'</span>';
269 $tpl->set(
'title', $out->getPageTitle() );
270 $tpl->set(
'pagetitle', $out->getHTMLTitle() );
271 $tpl->set(
'displaytitle', $out->mPageLinkTitle );
273 $tpl->set(
'thispage', $this->thispage );
274 $tpl->set(
'titleprefixeddbkey', $this->thispage );
275 $tpl->set(
'titletext',
$title->getText() );
276 $tpl->set(
'articleid',
$title->getArticleID() );
278 $tpl->set(
'isarticle', $out->isArticle() );
285 $tpl->set(
'feeds', count( $feeds ) ? $feeds :
false );
287 $tpl->set(
'mimetype', $config->get(
'MimeType' ) );
288 $tpl->set(
'charset',
'UTF-8' );
289 $tpl->set(
'wgScript', $config->get(
'Script' ) );
290 $tpl->set(
'skinname', $this->skinname );
291 $tpl->set(
'skinclass', static::class );
292 $tpl->set(
'skin', $this );
293 $tpl->set(
'stylename', $this->stylename );
294 $tpl->set(
'printable', $out->isPrintable() );
295 $tpl->set(
'handheld', $request->getBool(
'handheld' ) );
296 $tpl->set(
'loggedin', $this->loggedin );
297 $tpl->set(
'notspecialpage', !
$title->isSpecialPage() );
301 $tpl->set(
'searchaction', $searchLink );
304 $tpl->set(
'search', trim( $request->getVal(
'search' ) ) );
305 $tpl->set(
'stylepath', $config->get(
'StylePath' ) );
306 $tpl->set(
'articlepath', $config->get(
'ArticlePath' ) );
307 $tpl->set(
'scriptpath', $config->get(
'ScriptPath' ) );
308 $tpl->set(
'serverurl', $config->get(
'Server' ) );
310 $tpl->set(
'logopath', $logos[
'1x'] );
311 $tpl->set(
'sitename', $config->get(
'Sitename' ) );
314 $userLangCode = $userLang->getHtmlCode();
315 $userLangDir = $userLang->getDir();
317 $tpl->set(
'lang', $userLangCode );
318 $tpl->set(
'dir', $userLangDir );
319 $tpl->set(
'rtl', $userLang->isRTL() );
321 $tpl->set(
'capitalizeallnouns', $userLang->capitalizeAllNouns() ?
' capitalize-all-nouns' :
'' );
322 $tpl->set(
'showjumplinks',
true );
323 $tpl->set(
'username', $this->loggedin ? $this->username :
null );
324 $tpl->set(
'userpage', $this->userpage );
325 $tpl->set(
'userpageurl', $this->userpageUrlDetails[
'href'] );
326 $tpl->set(
'userlang', $userLangCode );
332 $tpl->set(
'specialpageattributes',
'' ); # obsolete
335 $tpl->set(
'prebodyhtml',
'' );
338 $tpl->set(
'logo', $this->
logoText() );
341 $tpl->set(
'copyright', $footerData[
'info'][
'copyright'] ??
false );
343 $tpl->set(
'viewcount',
false );
344 $tpl->set(
'lastmod', $footerData[
'info'][
'lastmod'] ??
false );
345 $tpl->set(
'credits', $footerData[
'info'][
'credits'] ??
false );
346 $tpl->set(
'numberofwatchingusers',
false );
351 $tpl->set(
'disclaimer', $footerData[
'places'][
'disclaimer'] ??
false );
352 $tpl->set(
'privacy', $footerData[
'places'][
'privacy'] ??
false );
353 $tpl->set(
'about', $footerData[
'places'][
'about'] ??
false );
356 $flattenedfooterlinks = [];
357 foreach ( $footerData as $category => $links ) {
358 $flattenedfooterlinks[$category] = array_keys( $links );
359 foreach ( $links as $key => $value ) {
362 $tpl->set( $key, $value );
365 $tpl->set(
'footerlinks', $flattenedfooterlinks );
368 $tpl->set(
'indicators', $out->getIndicators() );
373 $tpl->set(
'bodytext', $this->
wrapHTML(
$title, $out->getHTML() ) );
375 $tpl->set(
'language_urls', $this->
getLanguages() ?:
false );
383 unset( $content_navigation[
'user-menu'], $content_navigation[
'notifications'] );
385 $tpl->set(
'content_navigation', $content_navigation );
386 $tpl->set(
'content_actions', $content_actions );
395 $tpl->set(
'headelement', $out->headElement( $this ) );
397 $tpl->set(
'debug',
'' );
399 $tpl->set(
'reporttime',
wfReportTime( $out->getCSP()->getNonce() ) );
403 if ( !$this->getHookRunner()->onSkinTemplateOutputPageBeforeExec( $this, $tpl ) ) {
404 wfDebug( __METHOD__ .
": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!" );
409 $tpl->set(
'bodycontent', $tpl->data[
'bodytext'] );
416 [
'class' =>
'printfooter' ],
417 "\n{$tpl->data['printfooter']}"
419 $tpl->data[
'bodytext'] .= $tpl->data[
'debughtml'];
451 if ( $personalTools ===
null ) {
457 foreach ( $personalTools as $key => $item ) {
489 $pageurl =
$title->getLocalURL();
490 $services = MediaWikiServices::getInstance();
491 $authManager = $services->getAuthManager();
492 $permissionManager = $services->getPermissionManager();
497 # Due to T34276, if a user does not have read permissions,
498 # $this->getTitle() will just give Special:Badtitle, which is
499 # not especially useful as a returnto parameter. Use the title
500 # from the request instead, if there was one.
506 $page = $request->getVal(
'returnto', $page );
508 if ( strval( $page ) !==
'' ) {
509 $returnto[
'returnto'] = $page;
510 $query = $request->getVal(
'returntoquery', $this->thisquery );
513 if ( $query !=
'' ) {
514 $returnto[
'returntoquery'] = $query;
518 if ( $this->loggedin ) {
519 $personal_urls[
'userpage'] = [
521 'href' => &$this->userpageUrlDetails[
'href'],
522 'class' => $this->userpageUrlDetails[
'exists'] ? false :
'new',
523 'exists' => $this->userpageUrlDetails[
'exists'],
524 'active' => ( $this->userpageUrlDetails[
'href'] == $pageurl ),
529 if ( $includeNotifications ) {
532 $personal_urls += $contentNavigation[
'notifications'];
536 $personal_urls[
'mytalk'] = [
537 'text' => $this->
msg(
'mytalk' )->text(),
538 'href' => &$usertalkUrlDetails[
'href'],
539 'class' => $usertalkUrlDetails[
'exists'] ? false :
'new',
540 'exists' => $usertalkUrlDetails[
'exists'],
541 'active' => ( $usertalkUrlDetails[
'href'] == $pageurl )
544 $personal_urls[
'preferences'] = [
545 'text' => $this->
msg(
'mypreferences' )->text(),
547 'active' => ( $href == $pageurl )
550 if ( $this->
getAuthority()->isAllowed(
'viewmywatchlist' ) ) {
552 $personal_urls[
'watchlist'] = [
553 'text' => $this->
msg(
'mywatchlist' )->text(),
555 'active' => ( $href == $pageurl )
559 # We need to do an explicit check for Special:Contributions, as we
560 # have to match both the title, and the target, which could come
561 # from request values (Special:Contributions?target=Jimbo_Wales)
562 # or be specified in "sub page" form
563 # (Special:Contributions/Jimbo_Wales). The plot
564 # thickens, because the Title object is altered for special pages,
565 # so it doesn't contain the original alias-with-subpage.
568 list( $spName, $spPar ) =
569 MediaWikiServices::getInstance()->getSpecialPageFactory()->
570 resolveAlias( $origTitle->getText() );
571 $active = $spName ==
'Contributions'
579 $personal_urls[
'mycontris'] = [
580 'text' => $this->
msg(
'mycontris' )->text(),
586 if ( $request->getSession()->canSetUser() ) {
587 $personal_urls[
'logout'] = [
588 'text' => $this->
msg(
'pt-userlogout' )->text(),
589 'data-mw' =>
'interface',
593 (
$title->isSpecial(
'Preferences' ) ? [] : $returnto ) ),
598 $useCombinedLoginLink = $this->
getConfig()->get(
'UseCombinedLoginLink' );
599 if ( !$authManager->canCreateAccounts() || !$authManager->canAuthenticateNow() ) {
601 $useCombinedLoginLink =
false;
604 $loginlink = $this->
getAuthority()->isAllowed(
'createaccount' )
605 && $useCombinedLoginLink ?
'nav-login-createaccount' :
'pt-login';
608 'text' => $this->
msg( $loginlink )->text(),
610 'active' =>
$title->isSpecial(
'Userlogin' )
611 ||
$title->isSpecial(
'CreateAccount' ) && $useCombinedLoginLink,
613 $createaccount_url = [
614 'text' => $this->
msg(
'pt-createaccount' )->text(),
616 'active' =>
$title->isSpecial(
'CreateAccount' ),
621 if ( $permissionManager->groupHasPermission(
'*',
'edit' ) ) {
626 $personal_urls[
'anonuserpage'] = [
627 'text' => $this->
msg(
'notloggedin' )->text(),
636 $personal_urls[
'anontalk'] = [
637 'text' => $this->
msg(
'anontalk' )->text(),
641 $personal_urls[
'anoncontribs'] = [
642 'text' => $this->
msg(
'anoncontribs' )->text(),
649 $authManager->canCreateAccounts()
650 && $this->getAuthority()->isAllowed(
'createaccount' )
651 && !$useCombinedLoginLink
653 $personal_urls[
'createaccount'] = $createaccount_url;
656 if ( $authManager->canAuthenticateNow() ) {
658 $key = $permissionManager->groupHasPermission(
'*',
'read' )
661 $personal_urls[$key] = $login_url;
665 $this->getHookRunner()->onPersonalUrls( $personal_urls,
$title, $this );
667 return $personal_urls;
683 public function tabAction(
$title, $message, $selected, $query =
'', $checkEdit =
false ) {
686 $classes[] =
'selected';
689 if ( $checkEdit && !
$title->isKnown() ) {
692 if ( $query !==
'' ) {
693 $query =
'action=edit&redlink=1&' . $query;
695 $query =
'action=edit&redlink=1';
699 $services = MediaWikiServices::getInstance();
700 $linkClass = $services->getLinkRenderer()->getLinkClasses(
$title );
703 $msg =
new Message( $message );
704 $message = $message->getKey();
709 if ( is_array( $message ) ) {
711 $message = end( $message );
715 if ( $msg->exists() ) {
716 $text = $msg->text();
718 $text = $services->getLanguageConverterFactory()
719 ->getLanguageConverter( $services->getContentLanguage() )
721 $services->getNamespaceInfo()
722 ->getSubject(
$title->getNamespace() )
727 if ( !$this->getHookRunner()->onSkinTemplateTabAction( $this,
$title, $message,
728 $selected, $checkEdit, $classes, $query, $text, $result )
734 'class' => implode(
' ', $classes ),
736 'href' =>
$title->getLocalURL( $query ),
739 if ( $linkClass !==
'' ) {
740 $result[
'link-class'] = $linkClass;
753 if ( !is_object(
$title ) ) {
754 throw new MWException( __METHOD__ .
" given invalid pagename $name" );
759 'href' =>
$title->getLocalURL( $urlaction ),
760 'exists' =>
$title->isKnown(),
776 'href' =>
$title->getLocalURL( $urlaction ),
777 'exists' =>
$title->exists(),
793 $class =
'mw-watchlink ' . (
794 $onPage && ( $action ==
'watch' || $action ==
'unwatch' ) ?
'selected' :
''
798 if ( $this->
getConfig()->get(
'WatchlistExpiry' ) &&
801 $class .=
' mw-watchlink-temp';
807 'text' => $this->
msg( $mode )->text(),
808 'href' =>
$title->getLocalURL( [
'action' => $mode ] ),
859 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
861 $content_navigation = [
863 'notifications' => [],
871 $action = $request->getVal(
'action',
'view' );
875 $preventActiveTabs =
false;
876 $this->getHookRunner()->onSkinTemplatePreventOtherActiveTabs( $this, $preventActiveTabs );
879 if (
$title->canExist() ) {
881 $subjectPage =
$title->getSubjectPage();
882 $talkPage =
$title->getTalkPage();
885 $isTalk =
$title->isTalkPage();
888 $subjectId =
$title->getNamespaceKey(
'' );
890 if ( $subjectId ==
'main' ) {
893 $talkId =
"{$subjectId}_talk";
899 if ( $subjectId ===
'user' ) {
900 $subjectMsg =
wfMessage(
'nstab-user', $subjectPage->getRootText() );
902 $subjectMsg = [
"nstab-$subjectId" ];
904 if ( $subjectPage->isMainPage() ) {
905 array_unshift( $subjectMsg,
'mainpage-nstab' );
908 $content_navigation[
'namespaces'][$subjectId] = $this->
tabAction(
909 $subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs,
'', $userCanRead
911 $content_navigation[
'namespaces'][$subjectId][
'context'] =
'subject';
912 $content_navigation[
'namespaces'][$talkId] = $this->
tabAction(
913 $talkPage, [
"nstab-$talkId",
'talk' ], $isTalk && !$preventActiveTabs,
'', $userCanRead
915 $content_navigation[
'namespaces'][$talkId][
'context'] =
'talk';
917 if ( $userCanRead ) {
919 if (
$title->isKnown() ) {
920 $content_navigation[
'views'][
'view'] = $this->
tabAction(
921 $isTalk ? $talkPage : $subjectPage,
922 [
"$skname-view-view",
'view' ],
923 ( $onPage && ( $action ==
'view' || $action ==
'purge' ) ),
'',
true
926 $content_navigation[
'views'][
'view'][
'redundant'] =
true;
930 $isRemoteContent = $page && !$page->isLocal();
934 if ( $isRemoteContent ) {
935 $content_navigation[
'views'][
'view-foreign'] = [
939 params( $page->getWikiDisplayName() )->text(),
940 'href' => $page->getSourceURL(),
948 $this->getAuthority()->probablyCan(
'create',
$title ) )
951 $isTalkClass = $isTalk ?
' istalk' :
'';
953 $isEditing = $onPage && ( $action ==
'edit' || $action ==
'submit' );
956 $showNewSection = !$out->forceHideNewSectionLink()
957 && ( ( $isTalk && $out->isRevisionCurrent() ) || $out->showNewSectionLink() );
958 $section = $request->getVal(
'section' );
962 &&
$title->getDefaultMessageText() !== false
965 $msgKey = $isRemoteContent ?
'edit-local' :
'edit';
967 $msgKey = $isRemoteContent ?
'create-local' :
'create';
969 $content_navigation[
'views'][
'edit'] = [
970 'class' => ( $isEditing && ( $section !==
'new' || !$showNewSection )
977 'primary' => !$isRemoteContent,
981 if ( $showNewSection ) {
984 $content_navigation[
'views'][
'addsection'] = [
985 'class' => ( $isEditing && $section ==
'new' ) ?
'selected' :
false,
988 'href' =>
$title->getLocalURL(
'action=edit§ion=new' )
992 } elseif (
$title->hasSourceText() ) {
994 $content_navigation[
'views'][
'viewsource'] = [
995 'class' => ( $onPage && $action ==
'edit' ) ?
'selected' :
false,
1004 if (
$title->exists() ) {
1006 $content_navigation[
'views'][
'history'] = [
1007 'class' => ( $onPage && $action ==
'history' ) ?
'selected' :
false,
1010 'href' =>
$title->getLocalURL(
'action=history' ),
1014 $content_navigation[
'actions'][
'delete'] = [
1015 'class' => ( $onPage && $action ==
'delete' ) ?
'selected' :
false,
1018 'href' =>
$title->getLocalURL(
'action=delete' )
1024 $content_navigation[
'actions'][
'move'] = [
1025 'class' => $this->
getTitle()->isSpecial(
'Movepage' ) ?
'selected' :
false,
1028 'href' => $moveTitle->getLocalURL()
1034 $n =
$title->getDeletedEditsCount();
1040 'undelete' :
'viewdeleted';
1041 $content_navigation[
'actions'][
'undelete'] = [
1042 'class' => $this->
getTitle()->isSpecial(
'Undelete' ) ?
'selected' :
false,
1044 ->setContext( $this->
getContext() )->numParams( $n )->text(),
1045 'href' => $undelTitle->getLocalURL()
1052 $title->getRestrictionTypes() &&
1053 $permissionManager->getNamespaceRestrictionLevels(
$title->getNamespace(), $user ) !== [
'' ]
1055 $mode =
$title->isProtected() ?
'unprotect' :
'protect';
1056 $content_navigation[
'actions'][$mode] = [
1057 'class' => ( $onPage && $action == $mode ) ?
'selected' :
false,
1060 'href' =>
$title->getLocalURL(
"action=$mode" )
1066 ->isAllowedAll(
'viewmywatchlist',
'editmywatchlist' )
1077 $mode = $user->isWatched(
$title ) ?
'unwatch' :
'watch';
1090 $this->getHookRunner()->onSkinTemplateNavigation( $this, $content_navigation );
1092 $languageConverterFactory = MediaWikiServices::getInstance()->getLanguageConverterFactory();
1094 if ( $userCanRead && !$languageConverterFactory->isConversionDisabled() ) {
1095 $pageLang =
$title->getPageLanguage();
1096 $converter = $languageConverterFactory
1097 ->getLanguageConverter( $pageLang );
1100 if ( $converter->hasVariants() ) {
1102 $variants = $converter->getVariants();
1105 $preferred = $converter->getPreferredVariant();
1107 $params = $request->getQueryValues();
1108 unset( $params[
'title'] );
1113 foreach ( $variants as $code ) {
1115 $varname = $pageLang->getVariantname( $code );
1117 $content_navigation[
'variants'][] = [
1118 'class' => ( $code == $preferred ) ?
'selected' :
false,
1120 'href' =>
$title->getLocalURL( [
'variant' => $code ] + $params ),
1130 $url = $request->getRequestURL();
1134 $content_navigation[
'namespaces'][
'special'] = [
1135 'class' =>
'selected',
1136 'text' => $this->
msg(
'nstab-special' )->text(),
1138 'context' =>
'subject'
1140 $this->getHookRunner()->onSkinTemplateNavigation__SpecialPage(
1141 $this, $content_navigation );
1145 $this->getHookRunner()->onSkinTemplateNavigation__Universal(
1146 $this, $content_navigation );
1149 foreach ( $content_navigation as $section => &$links ) {
1150 foreach ( $links as $key => &$link ) {
1152 if ( isset( $link[
'id'] ) ) {
1156 if ( isset( $link[
'context'] ) && $link[
'context'] ==
'subject' ) {
1157 $xmlID =
'ca-nstab-' . $xmlID;
1158 } elseif ( isset( $link[
'context'] ) && $link[
'context'] ==
'talk' ) {
1160 $link[
'rel'] =
'discussion';
1161 } elseif ( $section ==
'variants' ) {
1162 $xmlID =
'ca-varlang-' . $xmlID;
1164 $xmlID =
'ca-' . $xmlID;
1166 $link[
'id'] = $xmlID;
1170 # We don't want to give the watch tab an accesskey if the
1171 # page is being edited, because that conflicts with the
1172 # accesskey on the watch checkbox. We also don't want to
1173 # give the edit tab an accesskey, because that's fairly
1174 # superfluous and conflicts with an accesskey (Ctrl-E) often
1175 # used for editing in Safari.
1176 if ( in_array( $action, [
'edit',
'submit' ] ) ) {
1177 if ( isset( $content_navigation[
'views'][
'edit'] ) ) {
1178 $content_navigation[
'views'][
'edit'][
'tooltiponly'] =
true;
1180 if ( isset( $content_navigation[
'actions'][
'watch'] ) ) {
1181 $content_navigation[
'actions'][
'watch'][
'tooltiponly'] =
true;
1183 if ( isset( $content_navigation[
'actions'][
'unwatch'] ) ) {
1184 $content_navigation[
'actions'][
'unwatch'][
'tooltiponly'] =
true;
1188 return $content_navigation;
1201 $content_actions = [];
1203 foreach ( $content_navigation as $navigation => $links ) {
1204 foreach ( $links as $key => $value ) {
1205 if ( isset( $value[
'redundant'] ) && $value[
'redundant'] ) {
1214 if ( isset( $value[
'id'] ) && substr( $value[
'id'], 0, 3 ) ==
'ca-' ) {
1215 $key = substr( $value[
'id'], 3 );
1218 if ( isset( $content_actions[$key] ) ) {
1219 wfDebug( __METHOD__ .
": Found a duplicate key for $key while flattening " .
1220 "content_navigation into content_actions." );
1224 $content_actions[$key] = $value;
1228 return $content_actions;
1238 $navUrls = parent::buildNavUrls();
1240 if ( !$out->isArticle() ) {
1243 $modifiedNavUrls = [];
1244 foreach ( $navUrls as $key => $url ) {
1245 $modifiedNavUrls[$key] = $url;
1246 if ( $key ===
'permalink' ) {
1247 $revid = $out->getRevisionId();
1250 $this->getHookRunner()->onSkinTemplateBuildNavUrlsNav_urlsAfterPermalink(
1251 $this, $modifiedNavUrls, $revid, $revid
1255 return $modifiedNavUrls;
1264 return $this->
getTitle()->getNamespaceKey();
1277 array $contentNavigation
1281 if ( isset( $contentNavigation[
'user-menu'][
'userpage'] ) ) {
1283 $contentNavigation[
'user-menu'],
1284 $contentNavigation[
'notifications'],
1288 return $contentNavigation[
'user-menu'];