MediaWiki  master
Skin.php
Go to the documentation of this file.
1 <?php
23 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
27 use Wikimedia\WrappedString;
28 use Wikimedia\WrappedStringList;
29 
42 abstract class Skin extends ContextSource {
43  use ProtectedHookAccessorTrait;
44 
48  protected $skinname = null;
49 
53  protected $options = [];
54  protected $mRelevantTitle = null;
55  protected $mRelevantUser = null;
56 
61  public $stylename = null;
62 
64  protected const VERSION_MAJOR = 1;
65 
73  public static function getVersion() {
74  return self::VERSION_MAJOR;
75  }
76 
83  public static function getSkinNames() {
84  wfDeprecated( __METHOD__, '1.36' );
85 
86  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
87  return $skinFactory->getSkinNames();
88  }
89 
99  public static function getAllowedSkins() {
100  wfDeprecated( __METHOD__, '1.36' );
101 
102  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
103  return $skinFactory->getAllowedSkins();
104  }
105 
115  public static function normalizeKey( $key ) {
117 
118  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
119  $skinNames = $skinFactory->getSkinNames();
120 
121  // Make keys lowercase for case-insensitive matching.
122  $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
123  $key = strtolower( $key );
124  $defaultSkin = strtolower( $wgDefaultSkin );
125  $fallbackSkin = strtolower( $wgFallbackSkin );
126 
127  if ( $key == '' || $key == 'default' ) {
128  // Don't return the default immediately;
129  // in a misconfiguration we need to fall back.
130  $key = $defaultSkin;
131  }
132 
133  if ( isset( $skinNames[$key] ) ) {
134  return $key;
135  }
136 
137  // Older versions of the software used a numeric setting
138  // in the user preferences.
139  $fallback = [
140  0 => $defaultSkin,
141  2 => 'cologneblue'
142  ];
143 
144  if ( isset( $fallback[$key] ) ) {
145  $key = $fallback[$key];
146  }
147 
148  if ( isset( $skinNames[$key] ) ) {
149  return $key;
150  } elseif ( isset( $skinNames[$defaultSkin] ) ) {
151  return $defaultSkin;
152  } else {
153  return $fallbackSkin;
154  }
155  }
156 
167  public function __construct( $options = null ) {
168  if ( is_string( $options ) ) {
169  $this->skinname = $options;
170  } elseif ( $options ) {
171  $name = $options['name'] ?? null;
172 
173  if ( !$name ) {
174  throw new SkinException( 'Skin name must be specified' );
175  }
176 
177  $this->options = $options;
178  $this->skinname = $name;
179  }
180  }
181 
185  public function getSkinName() {
186  return $this->skinname;
187  }
188 
198  public function isResponsive() {
199  return $this->options['responsive'] ?? false;
200  }
201 
206  public function initPage( OutputPage $out ) {
207  $skinMetaTags = $this->getConfig()->get( 'SkinMetaTags' );
208  $this->preloadExistence();
209 
210  if ( $this->isResponsive() ) {
211  $out->addMeta(
212  'viewport',
213  'width=device-width, initial-scale=1.0, ' .
214  'user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0'
215  );
216  }
217 
218  $tags = [
219  'og:title' => $out->getDisplayTitle(),
220  'twitter:card' => 'summary_large_image',
221  'og:type' => 'website',
222  ];
223 
224  // Support sharing on platforms such as Facebook and Twitter
225  foreach ( $tags as $key => $value ) {
226  if ( in_array( $key, $skinMetaTags ) ) {
227  $out->addMeta( $key, $value );
228  }
229  }
230  }
231 
243  public function getDefaultModules() {
244  $out = $this->getOutput();
245  $user = $this->getUser();
246 
247  // Modules declared in the $modules literal are loaded
248  // for ALL users, on ALL pages, in ALL skins.
249  // Keep this list as small as possible!
250  $modules = [
251  // The 'styles' key sets render-blocking style modules
252  // Unlike other keys in $modules, this is an associative array
253  // where each key is its own group pointing to a list of modules
254  'styles' => [
255  'skin' => $this->options['styles'] ?? [],
256  'core' => [],
257  'content' => [],
258  'syndicate' => [],
259  ],
260  'core' => [
261  'site',
262  'mediawiki.page.ready',
263  ],
264  // modules that enhance the content in some way
265  'content' => [],
266  // modules relating to search functionality
267  'search' => [],
268  // Skins can register their own scripts
269  'skin' => $this->options['scripts'] ?? [],
270  // modules relating to functionality relating to watching an article
271  'watch' => [],
272  // modules which relate to the current users preferences
273  'user' => [],
274  // modules relating to RSS/Atom Feeds
275  'syndicate' => [],
276  ];
277 
278  // Preload jquery.tablesorter for mediawiki.page.ready
279  if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
280  $modules['content'][] = 'jquery.tablesorter';
281  $modules['styles']['content'][] = 'jquery.tablesorter.styles';
282  }
283 
284  // Preload jquery.makeCollapsible for mediawiki.page.ready
285  if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
286  $modules['content'][] = 'jquery.makeCollapsible';
287  $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
288  }
289 
290  // Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
291  // on every page is deprecated. Express a dependency instead.
292  if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
293  $modules['styles']['content'][] = 'mediawiki.ui.button';
294  }
295 
296  if ( $out->isTOCEnabled() ) {
297  $modules['content'][] = 'mediawiki.toc';
298  }
299 
300  $authority = $this->getAuthority();
301  if ( $authority->getPerformer()->isRegistered()
302  && $authority->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
303  && $this->getRelevantTitle()->canExist()
304  ) {
305  $modules['watch'][] = 'mediawiki.page.watch.ajax';
306  }
307 
308  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
309  if ( $userOptionsLookup->getBoolOption( $user, 'editsectiononrightclick' )
310  || ( $out->isArticle() && $userOptionsLookup->getOption( $user, 'editondblclick' ) )
311  ) {
312  $modules['user'][] = 'mediawiki.misc-authed-pref';
313  }
314 
315  if ( $out->isSyndicated() ) {
316  $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
317  }
318 
319  return $modules;
320  }
321 
325  protected function preloadExistence() {
326  $titles = [];
327 
328  // User/talk link
329  $user = $this->getUser();
330  if ( $user->isRegistered() ) {
331  $titles[] = $user->getUserPage();
332  $titles[] = $user->getTalkPage();
333  }
334 
335  // Check, if the page can hold some kind of content, otherwise do nothing
336  $title = $this->getRelevantTitle();
337  if ( $title->canExist() && $title->canHaveTalkPage() ) {
338  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
339  if ( $title->isTalkPage() ) {
340  $titles[] = $namespaceInfo->getSubjectPage( $title );
341  } else {
342  $titles[] = $namespaceInfo->getTalkPage( $title );
343  }
344  }
345 
346  // Footer links (used by SkinTemplate::prepareQuickTemplate)
347  if ( $this->getConfig()->get( 'FooterLinkCacheExpiry' ) <= 0 ) {
348  $titles = array_merge(
349  $titles,
350  array_filter( [
351  $this->footerLinkTitle( 'privacy', 'privacypage' ),
352  $this->footerLinkTitle( 'aboutsite', 'aboutpage' ),
353  $this->footerLinkTitle( 'disclaimers', 'disclaimerpage' ),
354  ] )
355  );
356  }
357 
358  $this->getHookRunner()->onSkinPreloadExistence( $titles, $this );
359 
360  if ( $titles ) {
361  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
362  $lb = $linkBatchFactory->newLinkBatch( $titles );
363  $lb->setCaller( __METHOD__ );
364  $lb->execute();
365  }
366  }
367 
374  public function getRevisionId() {
375  wfDeprecated( __METHOD__, '1.34' );
376 
377  return $this->getOutput()->getRevisionId();
378  }
379 
386  public function isRevisionCurrent() {
387  wfDeprecated( __METHOD__, '1.34' );
388 
389  return $this->getOutput()->isRevisionCurrent();
390  }
391 
396  public function setRelevantTitle( $t ) {
397  $this->mRelevantTitle = $t;
398  }
399 
410  public function getRelevantTitle() {
411  return $this->mRelevantTitle ?? $this->getTitle();
412  }
413 
418  public function setRelevantUser( $u ) {
419  $this->mRelevantUser = $u;
420  }
421 
430  public function getRelevantUser() {
431  if ( isset( $this->mRelevantUser ) ) {
432  return $this->mRelevantUser;
433  }
434  $title = $this->getRelevantTitle();
435  if ( $title->hasSubjectNamespace( NS_USER ) ) {
436  $rootUser = $title->getRootText();
437  if ( User::isIP( $rootUser ) ) {
438  $this->mRelevantUser = User::newFromName( $rootUser, false );
439  } else {
440  $user = User::newFromName( $rootUser, false );
441 
442  if ( $user ) {
443  $user->load( User::READ_NORMAL );
444 
445  if ( $user->isRegistered() ) {
446  $this->mRelevantUser = $user;
447  }
448  }
449  }
450  return $this->mRelevantUser;
451  }
452  return null;
453  }
454 
458  abstract public function outputPage();
459 
466  public static function makeVariablesScript( $data, $nonce = null ) {
467  wfDeprecated( __METHOD__, '1.36' );
468 
469  if ( $data ) {
472  $nonce
473  );
474  }
475  return '';
476  }
477 
486  final public function doSetupSkinUserCss( OutputPage $out ) {
487  if ( MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'setupSkinUserCss', '1.32' ) ) {
488  $this->setupSkinUserCss( $out );
489  }
490  }
491 
500  public function setupSkinUserCss( OutputPage $out ) {
501  wfDeprecated( __METHOD__, '1.32' );
502  }
503 
509  public function getPageClasses( $title ) {
510  $numeric = 'ns-' . $title->getNamespace();
511 
512  if ( $title->isSpecialPage() ) {
513  $type = 'ns-special';
514  // T25315: provide a class based on the canonical special page name without subpages
515  list( $canonicalName ) = MediaWikiServices::getInstance()->getSpecialPageFactory()->
516  resolveAlias( $title->getDBkey() );
517  if ( $canonicalName ) {
518  $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
519  } else {
520  $type .= ' mw-invalidspecialpage';
521  }
522  } else {
523  if ( $title->isTalkPage() ) {
524  $type = 'ns-talk';
525  } else {
526  $type = 'ns-subject';
527  }
528  // T208315: add HTML class when the user can edit the page
529  if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
530  $type .= ' mw-editable';
531  }
532  }
533 
534  $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() );
535  $root = Sanitizer::escapeClass( 'rootpage-' . $title->getRootTitle()->getPrefixedText() );
536 
537  return "$numeric $type $name $root";
538  }
539 
544  public function getHtmlElementAttributes() {
545  $lang = $this->getLanguage();
546  return [
547  'lang' => $lang->getHtmlCode(),
548  'dir' => $lang->getDir(),
549  'class' => 'client-nojs',
550  ];
551  }
552 
559  protected function getLogo() {
560  wfDeprecated( __METHOD__, '1.36' );
561  return ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() )[ '1x' ];
562  }
563 
567  public function getCategoryLinks() {
568  $out = $this->getOutput();
569  $allCats = $out->getCategoryLinks();
570  $title = $this->getTitle();
571  $services = MediaWikiServices::getInstance();
572  $linkRenderer = $services->getLinkRenderer();
573 
574  if ( $allCats === [] ) {
575  return '';
576  }
577 
578  $embed = "<li>";
579  $pop = "</li>";
580 
581  $s = '';
582  $colon = $this->msg( 'colon-separator' )->escaped();
583 
584  if ( !empty( $allCats['normal'] ) ) {
585  $t = $embed . implode( $pop . $embed, $allCats['normal'] ) . $pop;
586 
587  $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) );
588  $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
589  $pageCategoriesLinkTitle = Title::newFromText( $linkPage );
590  if ( $pageCategoriesLinkTitle ) {
591  $link = $linkRenderer->makeLink( $pageCategoriesLinkTitle, $msg->text() );
592  } else {
593  $link = $msg->escaped();
594  }
595  $s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' .
596  $link . $colon . '<ul>' . $t . '</ul></div>';
597  }
598 
599  # Hidden categories
600  if ( isset( $allCats['hidden'] ) ) {
601  $userOptionsLookup = $services->getUserOptionsLookup();
602 
603  if ( $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' ) ) {
604  $class = ' mw-hidden-cats-user-shown';
605  } elseif ( $title->inNamespace( NS_CATEGORY ) ) {
606  $class = ' mw-hidden-cats-ns-shown';
607  } else {
608  $class = ' mw-hidden-cats-hidden';
609  }
610 
611  $s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" .
612  $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
613  $colon . '<ul>' . $embed . implode( $pop . $embed, $allCats['hidden'] ) . $pop . '</ul>' .
614  '</div>';
615  }
616 
617  # optional 'dmoz-like' category browser. Will be shown under the list
618  # of categories an article belong to
619  if ( $this->getConfig()->get( 'UseCategoryBrowser' ) ) {
620  $s .= '<br /><hr />';
621 
622  # get a big array of the parents tree
623  $parenttree = $title->getParentCategoryTree();
624  # Skin object passed by reference cause it can not be
625  # accessed under the method subfunction drawCategoryBrowser
626  $tempout = explode( "\n", $this->drawCategoryBrowser( $parenttree ) );
627  # Clean out bogus first entry and sort them
628  unset( $tempout[0] );
629  asort( $tempout );
630  # Output one per line
631  $s .= implode( "<br />\n", $tempout );
632  }
633 
634  return $s;
635  }
636 
642  protected function drawCategoryBrowser( $tree ) {
643  $return = '';
644  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
645 
646  foreach ( $tree as $element => $parent ) {
647  if ( empty( $parent ) ) {
648  # element start a new list
649  $return .= "\n";
650  } else {
651  # grab the others elements
652  $return .= $this->drawCategoryBrowser( $parent ) . ' &gt; ';
653  }
654 
655  # add our current element to the list
656  $eltitle = Title::newFromText( $element );
657  $return .= $linkRenderer->makeLink( $eltitle, $eltitle->getText() );
658  }
659 
660  return $return;
661  }
662 
666  public function getCategories() {
667  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
668  $showHiddenCats = $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' );
669 
670  $catlinks = $this->getCategoryLinks();
671  // Check what we're showing
672  $allCats = $this->getOutput()->getCategoryLinks();
673  $showHidden = $showHiddenCats || $this->getTitle()->inNamespace( NS_CATEGORY );
674 
675  $classes = [ 'catlinks' ];
676  if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
677  $classes[] = 'catlinks-allhidden';
678  }
679 
680  return Html::rawElement(
681  'div',
682  [ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
683  $catlinks
684  );
685  }
686 
701  protected function afterContentHook() {
702  $data = '';
703 
704  if ( $this->getHookRunner()->onSkinAfterContent( $data, $this ) ) {
705  // adding just some spaces shouldn't toggle the output
706  // of the whole <div/>, so we use trim() here
707  if ( trim( $data ) != '' ) {
708  // Doing this here instead of in the skins to
709  // ensure that the div has the same ID in all
710  // skins
711  $data = "<div id='mw-data-after-content'>\n" .
712  "\t$data\n" .
713  "</div>\n";
714  }
715  } else {
716  wfDebug( "Hook SkinAfterContent changed output processing." );
717  }
718 
719  return $data;
720  }
721 
729  protected function generateDebugHTML() {
730  wfDeprecated( __METHOD__, '1.35' );
731  return MWDebug::getHTMLDebugLog();
732  }
733 
739  public function bottomScripts() {
740  // TODO and the suckage continues. This function is really just a wrapper around
741  // OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
742  // up at some point
743  $chunks = [ $this->getOutput()->getBottomScripts() ];
744 
745  // Keep the hook appendage separate to preserve WrappedString objects.
746  // This enables BaseTemplate::getTrail() to merge them where possible.
747  $extraHtml = '';
748  $this->getHookRunner()->onSkinAfterBottomScripts( $this, $extraHtml );
749  if ( $extraHtml !== '' ) {
750  $chunks[] = $extraHtml;
751  }
752  return WrappedString::join( "\n", $chunks );
753  }
754 
762  public function printSource() {
763  $title = $this->getTitle();
764  $oldid = $this->getOutput()->getRevisionId();
765  if ( $oldid ) {
766  $canonicalUrl = $title->getCanonicalURL( 'oldid=' . $oldid );
767  $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
768  } else {
769  // oldid not available for non existing pages
770  $url = htmlspecialchars( wfExpandIRI( $title->getCanonicalURL() ) );
771  }
772 
773  return $this->msg( 'retrievedfrom' )
774  ->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
775  ->parse();
776  }
777 
781  public function getUndeleteLink() {
782  $action = $this->getRequest()->getVal( 'action', 'view' );
783  $title = $this->getTitle();
784  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
785 
786  if ( ( !$title->exists() || $action == 'history' ) &&
787  $this->getAuthority()->probablyCan( 'deletedhistory', $title )
788  ) {
789  $n = $title->getDeletedEditsCount();
790 
791  if ( $n ) {
792  if ( $this->getAuthority()->probablyCan( 'undelete', $title ) ) {
793  $msg = 'thisisdeleted';
794  } else {
795  $msg = 'viewdeleted';
796  }
797 
798  $subtitle = $this->msg( $msg )->rawParams(
799  $linkRenderer->makeKnownLink(
800  SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ),
801  $this->msg( 'restorelink' )->numParams( $n )->text() )
802  )->escaped();
803 
804  $links = [];
805  // Add link to page logs, unless we're on the history page (which
806  // already has one)
807  if ( $action !== 'history' ) {
808  $links[] = $linkRenderer->makeKnownLink(
809  SpecialPage::getTitleFor( 'Log' ),
810  $this->msg( 'viewpagelogs-lowercase' )->text(),
811  [],
812  [ 'page' => $title->getPrefixedText() ]
813  );
814  }
815 
816  // Allow extensions to add more links
817  $this->getHookRunner()->onUndeletePageToolLinks(
818  $this->getContext(), $linkRenderer, $links );
819 
820  if ( $links ) {
821  $subtitle .= ''
822  . $this->msg( 'word-separator' )->escaped()
823  . $this->msg( 'parentheses' )
824  ->rawParams( $this->getLanguage()->pipeList( $links ) )
825  ->escaped();
826  }
827 
828  return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
829  }
830  }
831 
832  return '';
833  }
834 
840  public function subPageSubtitle( $out ) {
841  wfDeprecated( __METHOD__, '1.36' );
842  return $this->subPageSubtitleInternal();
843  }
844 
848  private function subPageSubtitleInternal() {
849  $services = MediaWikiServices::getInstance();
850  $linkRenderer = $services->getLinkRenderer();
851  $out = $this->getOutput();
852  $title = $out->getTitle();
853  $subpages = '';
854 
855  if ( !$this->getHookRunner()->onSkinSubPageSubtitle( $subpages, $this, $out ) ) {
856  return $subpages;
857  }
858 
859  $hasSubpages = $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() );
860  if ( !$out->isArticle() || !$hasSubpages ) {
861  return $subpages;
862  }
863 
864  $ptext = $title->getPrefixedText();
865  if ( strpos( $ptext, '/' ) !== false ) {
866  $links = explode( '/', $ptext );
867  array_pop( $links );
868  $count = 0;
869  $growingLink = '';
870  $display = '';
871  $lang = $this->getLanguage();
872 
873  foreach ( $links as $link ) {
874  $growingLink .= $link;
875  $display .= $link;
876  $linkObj = Title::newFromText( $growingLink );
877 
878  if ( $linkObj && $linkObj->isKnown() ) {
879  $getlink = $linkRenderer->makeKnownLink( $linkObj, $display );
880 
881  $count++;
882 
883  if ( $count > 1 ) {
884  $subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
885  } else {
886  $subpages .= '&lt; ';
887  }
888 
889  $subpages .= $getlink;
890  $display = '';
891  } else {
892  $display .= '/';
893  }
894  $growingLink .= '/';
895  }
896  }
897 
898  return $subpages;
899  }
900 
905  protected function getSearchLink() {
906  wfDeprecated( __METHOD__, '1.36' );
907 
908  $searchPage = SpecialPage::getTitleFor( 'Search' );
909  return $searchPage->getLocalURL();
910  }
911 
916  public function getCopyright( $type = 'detect' ) {
917  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
918  if ( $type == 'detect' ) {
919  if ( !$this->getOutput()->isRevisionCurrent()
920  && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
921  ) {
922  $type = 'history';
923  } else {
924  $type = 'normal';
925  }
926  }
927 
928  if ( $type == 'history' ) {
929  $msg = 'history_copyright';
930  } else {
931  $msg = 'copyright';
932  }
933 
934  $config = $this->getConfig();
935 
936  if ( $config->get( 'RightsPage' ) ) {
937  $title = Title::newFromText( $config->get( 'RightsPage' ) );
938  $link = $linkRenderer->makeKnownLink(
939  $title, new HtmlArmor( $config->get( 'RightsText' ) ?: $title->getText() )
940  );
941  } elseif ( $config->get( 'RightsUrl' ) ) {
942  $link = Linker::makeExternalLink( $config->get( 'RightsUrl' ), $config->get( 'RightsText' ) );
943  } elseif ( $config->get( 'RightsText' ) ) {
944  $link = $config->get( 'RightsText' );
945  } else {
946  # Give up now
947  return '';
948  }
949 
950  // Allow for site and per-namespace customization of copyright notice.
951  $this->getHookRunner()->onSkinCopyrightFooter( $this->getTitle(), $type, $msg, $link );
952 
953  return $this->msg( $msg )->rawParams( $link )->text();
954  }
955 
959  protected function getCopyrightIcon() {
960  $out = '';
961  $config = $this->getConfig();
962 
963  $footerIcons = $config->get( 'FooterIcons' );
964  if ( $footerIcons['copyright']['copyright'] ) {
965  $out = $footerIcons['copyright']['copyright'];
966  } elseif ( $config->get( 'RightsIcon' ) ) {
967  $icon = htmlspecialchars( $config->get( 'RightsIcon' ) );
968  $url = $config->get( 'RightsUrl' );
969 
970  if ( $url ) {
971  $out .= '<a href="' . htmlspecialchars( $url ) . '">';
972  }
973 
974  $text = htmlspecialchars( $config->get( 'RightsText' ) );
975  $out .= "<img src=\"$icon\" alt=\"$text\" width=\"88\" height=\"31\" />";
976 
977  if ( $url ) {
978  $out .= '</a>';
979  }
980  }
981 
982  return $out;
983  }
984 
989  protected function getPoweredBy() {
990  $resourceBasePath = $this->getConfig()->get( 'ResourceBasePath' );
991  $url1 = htmlspecialchars(
992  "$resourceBasePath/resources/assets/poweredby_mediawiki_88x31.png"
993  );
994  $url1_5 = htmlspecialchars(
995  "$resourceBasePath/resources/assets/poweredby_mediawiki_132x47.png"
996  );
997  $url2 = htmlspecialchars(
998  "$resourceBasePath/resources/assets/poweredby_mediawiki_176x62.png"
999  );
1000  $text = '<a href="https://www.mediawiki.org/"><img src="' . $url1
1001  . '" srcset="' . $url1_5 . ' 1.5x, ' . $url2 . ' 2x" '
1002  . 'height="31" width="88" alt="Powered by MediaWiki" loading="lazy" /></a>';
1003  $this->getHookRunner()->onSkinGetPoweredBy( $text, $this );
1004  return $text;
1005  }
1006 
1012  protected function lastModified() {
1013  $timestamp = $this->getOutput()->getRevisionTimestamp();
1014  $user = $this->getUser();
1015  $language = $this->getLanguage();
1016 
1017  # No cached timestamp, load it from the database
1018  if ( $timestamp === null ) {
1019  $revId = $this->getOutput()->getRevisionId();
1020  if ( $revId !== null ) {
1021  $timestamp = MediaWikiServices::getInstance()
1022  ->getRevisionLookup()
1023  ->getTimestampFromId( $revId );
1024  }
1025  }
1026 
1027  if ( $timestamp ) {
1028  $d = $language->userDate( $timestamp, $user );
1029  $t = $language->userTime( $timestamp, $user );
1030  $s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->parse();
1031  } else {
1032  $s = '';
1033  }
1034 
1035  if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getLaggedReplicaMode() ) {
1036  $s .= ' <strong>' . $this->msg( 'laggedreplicamode' )->parse() . '</strong>';
1037  }
1038 
1039  return $s;
1040  }
1041 
1046  public function logoText( $align = '' ) {
1047  if ( $align != '' ) {
1048  $a = " style='float: {$align};'";
1049  } else {
1050  $a = '';
1051  }
1052 
1053  $mp = $this->msg( 'mainpage' )->escaped();
1054  $mptitle = Title::newMainPage();
1055  $url = ( is_object( $mptitle ) ? htmlspecialchars( $mptitle->getLocalURL() ) : '' );
1056 
1057  $logourl = ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() )[ '1x' ];
1058  return "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
1059  }
1060 
1069  public function makeFooterIcon( $icon, $withImage = 'withImage' ) {
1070  if ( is_string( $icon ) ) {
1071  $html = $icon;
1072  } else { // Assuming array
1073  $url = $icon["url"] ?? null;
1074  unset( $icon["url"] );
1075  if ( isset( $icon["src"] ) && $withImage === 'withImage' ) {
1076  // Lazy-load footer icons, since they're not part of the printed view.
1077  $icon["loading"] = 'lazy';
1078  // do this the lazy way, just pass icon data as an attribute array
1079  $html = Html::element( 'img', $icon );
1080  } else {
1081  $html = htmlspecialchars( $icon["alt"] );
1082  }
1083  if ( $url ) {
1084  $html = Html::rawElement( 'a',
1085  [ "href" => $url, "target" => $this->getConfig()->get( 'ExternalLinkTarget' ) ],
1086  $html );
1087  }
1088  }
1089  return $html;
1090  }
1091 
1098  public function mainPageLink() {
1099  wfDeprecated( __METHOD__, '1.36' );
1100 
1101  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1102  $s = $linkRenderer->makeKnownLink(
1104  $this->msg( 'mainpage' )->text()
1105  );
1106 
1107  return $s;
1108  }
1109 
1127  public function footerLink( $desc, $page ) {
1128  $title = $this->footerLinkTitle( $desc, $page );
1129 
1130  if ( !$title ) {
1131  return '';
1132  }
1133 
1134  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1135  return $linkRenderer->makeKnownLink(
1136  $title,
1137  $this->msg( $desc )->text()
1138  );
1139  }
1140 
1146  private function footerLinkTitle( $desc, $page ) {
1147  // If the link description has been disabled in the default language,
1148  if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
1149  // then it is disabled, for all languages.
1150  return null;
1151  }
1152  // Otherwise, we display the link for the user, described in their
1153  // language (which may or may not be the same as the default language),
1154  // but we make the link target be the one site-wide page.
1155  $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
1156 
1157  return $title ?: null;
1158  }
1159 
1166  public function getSiteFooterLinks() {
1167  $callback = function () {
1168  return [
1169  'privacy' => $this->footerLink( 'privacy', 'privacypage' ),
1170  'about' => $this->footerLink( 'aboutsite', 'aboutpage' ),
1171  'disclaimer' => $this->footerLink( 'disclaimers', 'disclaimerpage' )
1172  ];
1173  };
1174 
1175  $services = MediaWikiServices::getInstance();
1176  $msgCache = $services->getMessageCache();
1177  $wanCache = $services->getMainWANObjectCache();
1178  $config = $this->getConfig();
1179 
1180  return ( $config->get( 'FooterLinkCacheExpiry' ) > 0 )
1181  ? $wanCache->getWithSetCallback(
1182  $wanCache->makeKey( 'footer-links' ),
1183  $config->get( 'FooterLinkCacheExpiry' ),
1184  $callback,
1185  [
1186  'checkKeys' => [
1187  // Unless there is both no exact $code override nor an i18n definition
1188  // in the software, the only MediaWiki page to check is for $code.
1189  $msgCache->getCheckKey( $this->getLanguage()->getCode() )
1190  ],
1191  'lockTSE' => 30
1192  ]
1193  )
1194  : $callback();
1195  }
1196 
1203  public function privacyLink() {
1204  wfDeprecated( __METHOD__, '1.36' );
1205  return $this->footerLink( 'privacy', 'privacypage' );
1206  }
1207 
1214  public function aboutLink() {
1215  wfDeprecated( __METHOD__, '1.36' );
1216  return $this->footerLink( 'aboutsite', 'aboutpage' );
1217  }
1218 
1225  public function disclaimerLink() {
1226  wfDeprecated( __METHOD__, '1.36' );
1227  return $this->footerLink( 'disclaimers', 'disclaimerpage' );
1228  }
1229 
1237  public function editUrlOptions() {
1238  $options = [ 'action' => 'edit' ];
1239  $out = $this->getOutput();
1240 
1241  if ( !$out->isRevisionCurrent() ) {
1242  $options['oldid'] = intval( $out->getRevisionId() );
1243  }
1244 
1245  return $options;
1246  }
1247 
1252  public function showEmailUser( $id ) {
1253  if ( $id instanceof User ) {
1254  $targetUser = $id;
1255  } else {
1256  $targetUser = User::newFromId( $id );
1257  }
1258 
1259  # The sending user must have a confirmed email address and the receiving
1260  # user must accept emails from the sender.
1261  return $this->getUser()->canSendEmail()
1262  && SpecialEmailUser::validateTarget( $targetUser, $this->getUser() ) === '';
1263  }
1264 
1278  public function getSkinStylePath( $name ) {
1279  if ( $this->stylename === null ) {
1280  $class = static::class;
1281  throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
1282  }
1283 
1284  return $this->getConfig()->get( 'StylePath' ) . "/{$this->stylename}/$name";
1285  }
1286 
1287  /* these are used extensively in SkinTemplate, but also some other places */
1288 
1293  public static function makeMainPageUrl( $urlaction = '' ) {
1295  self::checkTitle( $title, '' );
1296 
1297  return $title->getLinkURL( $urlaction );
1298  }
1299 
1311  public static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1313  if ( $proto === null ) {
1314  return $title->getLocalURL( $urlaction );
1315  } else {
1316  return $title->getFullURL( $urlaction, false, $proto );
1317  }
1318  }
1319 
1326  public static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1327  $title = SpecialPage::getSafeTitleFor( $name, $subpage );
1328  return $title->getLocalURL( $urlaction );
1329  }
1330 
1337  public static function makeUrl( $name, $urlaction = '' ) {
1338  wfDeprecated( __METHOD__, '1.36' );
1339 
1340  $title = Title::newFromText( $name );
1341  self::checkTitle( $title, $name );
1342  return $title->getLocalURL( $urlaction );
1343  }
1344 
1351  public static function makeInternalOrExternalUrl( $name ) {
1352  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
1353  return $name;
1354  } else {
1355  $title = Title::newFromText( $name );
1356  self::checkTitle( $title, $name );
1357  return $title->getLocalURL();
1358  }
1359  }
1360 
1369  public static function makeNSUrl( $name, $urlaction = '', $namespace = NS_MAIN ) {
1370  wfDeprecated( __METHOD__, '1.35' );
1371  $title = Title::makeTitleSafe( $namespace, $name );
1372  self::checkTitle( $title, $name );
1373 
1374  return $title->getLocalURL( $urlaction );
1375  }
1376 
1383  protected static function makeUrlDetails( $name, $urlaction = '' ) {
1384  $title = Title::newFromText( $name );
1385  self::checkTitle( $title, $name );
1386 
1387  return [
1388  'href' => $title->getLocalURL( $urlaction ),
1389  'exists' => $title->isKnown(),
1390  ];
1391  }
1392 
1399  protected static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1400  $title = Title::newFromText( $name );
1401  self::checkTitle( $title, $name );
1402 
1403  return [
1404  'href' => $title->getLocalURL( $urlaction ),
1405  'exists' => true
1406  ];
1407  }
1408 
1415  public static function checkTitle( &$title, $name ) {
1416  if ( !is_object( $title ) ) {
1417  $title = Title::newFromText( $name );
1418  if ( !is_object( $title ) ) {
1419  $title = Title::newFromText( '--error: link target missing--' );
1420  }
1421  }
1422  }
1423 
1432  public function mapInterwikiToLanguage( $code ) {
1433  $map = $this->getConfig()->get( 'InterlanguageLinkCodeMap' );
1434  return $map[ $code ] ?? $code;
1435  }
1436 
1445  public function getLanguages() {
1446  if ( $this->getConfig()->get( 'HideInterlanguageLinks' ) ) {
1447  return [];
1448  }
1449  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1450 
1451  $userLang = $this->getLanguage();
1452  $languageLinks = [];
1453  $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1454 
1455  foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
1456  $class = 'interlanguage-link interwiki-' . explode( ':', $languageLinkText, 2 )[0];
1457 
1458  $languageLinkTitle = Title::newFromText( $languageLinkText );
1459  if ( !$languageLinkTitle ) {
1460  continue;
1461  }
1462 
1463  $ilInterwikiCode = $this->mapInterwikiToLanguage( $languageLinkTitle->getInterwiki() );
1464 
1465  $ilLangName = $langNameUtils->getLanguageName( $ilInterwikiCode );
1466 
1467  if ( strval( $ilLangName ) === '' ) {
1468  $ilDisplayTextMsg = $this->msg( "interlanguage-link-$ilInterwikiCode" );
1469  if ( !$ilDisplayTextMsg->isDisabled() ) {
1470  // Use custom MW message for the display text
1471  $ilLangName = $ilDisplayTextMsg->text();
1472  } else {
1473  // Last resort: fallback to the language link target
1474  $ilLangName = $languageLinkText;
1475  }
1476  } else {
1477  // Use the language autonym as display text
1478  $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
1479  }
1480 
1481  // CLDR extension or similar is required to localize the language name;
1482  // otherwise we'll end up with the autonym again.
1483  $ilLangLocalName = $langNameUtils->getLanguageName(
1484  $ilInterwikiCode,
1485  $userLang->getCode()
1486  );
1487 
1488  $languageLinkTitleText = $languageLinkTitle->getText();
1489  if ( $ilLangLocalName === '' ) {
1490  $ilFriendlySiteName = $this->msg( "interlanguage-link-sitename-$ilInterwikiCode" );
1491  if ( !$ilFriendlySiteName->isDisabled() ) {
1492  if ( $languageLinkTitleText === '' ) {
1493  $ilTitle = $this->msg(
1494  'interlanguage-link-title-nonlangonly',
1495  $ilFriendlySiteName->text()
1496  )->text();
1497  } else {
1498  $ilTitle = $this->msg(
1499  'interlanguage-link-title-nonlang',
1500  $languageLinkTitleText,
1501  $ilFriendlySiteName->text()
1502  )->text();
1503  }
1504  } else {
1505  // we have nothing friendly to put in the title, so fall back to
1506  // displaying the interlanguage link itself in the title text
1507  // (similar to what is done in page content)
1508  $ilTitle = $languageLinkTitle->getInterwiki() .
1509  ":$languageLinkTitleText";
1510  }
1511  } elseif ( $languageLinkTitleText === '' ) {
1512  $ilTitle = $this->msg(
1513  'interlanguage-link-title-langonly',
1514  $ilLangLocalName
1515  )->text();
1516  } else {
1517  $ilTitle = $this->msg(
1518  'interlanguage-link-title',
1519  $languageLinkTitleText,
1520  $ilLangLocalName
1521  )->text();
1522  }
1523 
1524  $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
1525  $languageLink = [
1526  'href' => $languageLinkTitle->getFullURL(),
1527  'text' => $ilLangName,
1528  'title' => $ilTitle,
1529  'class' => $class,
1530  'link-class' => 'interlanguage-link-target',
1531  'lang' => $ilInterwikiCodeBCP47,
1532  'hreflang' => $ilInterwikiCodeBCP47,
1533  ];
1534  $hookContainer->run(
1535  'SkinTemplateGetLanguageLink',
1536  [ &$languageLink, $languageLinkTitle, $this->getTitle(), $this->getOutput() ],
1537  []
1538  );
1539  $languageLinks[] = $languageLink;
1540  }
1541 
1542  return $languageLinks;
1543  }
1544 
1551  protected function buildNavUrls() {
1552  $out = $this->getOutput();
1553  $title = $this->getTitle();
1554  $thispage = $title->getPrefixedDBkey();
1555  $uploadNavigationUrl = $this->getConfig()->get( 'UploadNavigationUrl' );
1556 
1557  $nav_urls = [];
1558  $nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1559  if ( $uploadNavigationUrl ) {
1560  $nav_urls['upload'] = [ 'href' => $uploadNavigationUrl ];
1561  } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
1562  $nav_urls['upload'] = [ 'href' => self::makeSpecialUrl( 'Upload' ) ];
1563  } else {
1564  $nav_urls['upload'] = false;
1565  }
1566  $nav_urls['specialpages'] = [ 'href' => self::makeSpecialUrl( 'Specialpages' ) ];
1567 
1568  $nav_urls['print'] = false;
1569  $nav_urls['permalink'] = false;
1570  $nav_urls['info'] = false;
1571  $nav_urls['whatlinkshere'] = false;
1572  $nav_urls['recentchangeslinked'] = false;
1573  $nav_urls['contributions'] = false;
1574  $nav_urls['log'] = false;
1575  $nav_urls['blockip'] = false;
1576  $nav_urls['mute'] = false;
1577  $nav_urls['emailuser'] = false;
1578  $nav_urls['userrights'] = false;
1579 
1580  // A print stylesheet is attached to all pages, but nobody ever
1581  // figures that out. :) Add a link...
1582  if ( !$out->isPrintable() && ( $out->isArticle() || $title->isSpecialPage() ) ) {
1583  $nav_urls['print'] = [
1584  'text' => $this->msg( 'printableversion' )->text(),
1585  'href' => 'javascript:print();'
1586  ];
1587  }
1588 
1589  if ( $out->isArticle() ) {
1590  // Also add a "permalink" while we're at it
1591  $revid = $out->getRevisionId();
1592  if ( $revid ) {
1593  $nav_urls['permalink'] = [
1594  'text' => $this->msg( 'permalink' )->text(),
1595  'href' => $title->getLocalURL( "oldid=$revid" )
1596  ];
1597  }
1598  }
1599 
1600  if ( $out->isArticleRelated() ) {
1601  $nav_urls['whatlinkshere'] = [
1602  'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $thispage )->getLocalURL()
1603  ];
1604 
1605  $nav_urls['info'] = [
1606  'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1607  'href' => $title->getLocalURL( "action=info" )
1608  ];
1609 
1610  if ( $title->exists() || $title->inNamespace( NS_CATEGORY ) ) {
1611  $nav_urls['recentchangeslinked'] = [
1612  'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $thispage )->getLocalURL()
1613  ];
1614  }
1615  }
1616 
1617  $user = $this->getRelevantUser();
1618 
1619  // The relevant user should only be set if it exists. However, if it exists but is hidden,
1620  // and the viewer cannot see hidden users, this exposes the fact that the user exists;
1621  // pretend like the user does not exist in such cases, by setting $user to null, which
1622  // is what getRelevantUser returns if there is no user set (though it is documented as
1623  // always returning a User...) See T120883
1624  if ( $user && $user->isRegistered() && $user->isHidden() &&
1625  !$this->getAuthority()->isAllowed( 'hideuser' )
1626  ) {
1627  $user = null;
1628  }
1629 
1630  if ( $user ) {
1631  $rootUser = $user->getName();
1632 
1633  $nav_urls['contributions'] = [
1634  'text' => $this->msg( 'tool-link-contributions', $rootUser )->text(),
1635  'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1636  'tooltip-params' => [ $rootUser ],
1637  ];
1638 
1639  $nav_urls['log'] = [
1640  'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
1641  ];
1642 
1643  if ( $this->getAuthority()->isAllowed( 'block' ) ) {
1644  $nav_urls['blockip'] = [
1645  'text' => $this->msg( 'blockip', $rootUser )->text(),
1646  'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser )
1647  ];
1648  }
1649 
1650  if ( $this->showEmailUser( $user ) ) {
1651  $nav_urls['emailuser'] = [
1652  'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1653  'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1654  'tooltip-params' => [ $rootUser ],
1655  ];
1656  }
1657 
1658  if ( !$user->isAnon() ) {
1659  if ( $this->getUser()->isRegistered() && $this->getConfig()->get( 'EnableSpecialMute' ) ) {
1660  $nav_urls['mute'] = [
1661  'text' => $this->msg( 'mute-preferences' )->text(),
1662  'href' => self::makeSpecialUrlSubpage( 'Mute', $rootUser )
1663  ];
1664  }
1665 
1666  $sur = new UserrightsPage;
1667  $sur->setContext( $this->getContext() );
1668  $canChange = $sur->userCanChangeRights( $user );
1669  $nav_urls['userrights'] = [
1670  'text' => $this->msg(
1671  $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly',
1672  $rootUser
1673  )->text(),
1674  'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
1675  ];
1676  }
1677  }
1678 
1679  return $nav_urls;
1680  }
1681 
1687  final protected function buildFeedUrls() {
1688  $feeds = [];
1689  $out = $this->getOutput();
1690  if ( $out->isSyndicated() ) {
1691  foreach ( $out->getSyndicationLinks() as $format => $link ) {
1692  $feeds[$format] = [
1693  // Messages: feed-atom, feed-rss
1694  'text' => $this->msg( "feed-$format" )->text(),
1695  'href' => $link
1696  ];
1697  }
1698  }
1699  return $feeds;
1700  }
1701 
1726  public function buildSidebar() {
1727  $services = MediaWikiServices::getInstance();
1728  $callback = function ( $old = null, &$ttl = null ) {
1729  $bar = [];
1730  $this->addToSidebar( $bar, 'sidebar' );
1731  $this->getHookRunner()->onSkinBuildSidebar( $this, $bar );
1732  $msgCache = MediaWikiServices::getInstance()->getMessageCache();
1733  if ( $msgCache->isDisabled() ) {
1734  $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
1735  }
1736 
1737  return $bar;
1738  };
1739 
1740  $msgCache = $services->getMessageCache();
1741  $wanCache = $services->getMainWANObjectCache();
1742  $config = $this->getConfig();
1743  $languageCode = $this->getLanguage()->getCode();
1744 
1745  $sidebar = $config->get( 'EnableSidebarCache' )
1746  ? $wanCache->getWithSetCallback(
1747  $wanCache->makeKey( 'sidebar', $languageCode ),
1748  $config->get( 'SidebarCacheExpiry' ),
1749  $callback,
1750  [
1751  'checkKeys' => [
1752  // Unless there is both no exact $code override nor an i18n definition
1753  // in the software, the only MediaWiki page to check is for $code.
1754  $msgCache->getCheckKey( $languageCode )
1755  ],
1756  'lockTSE' => 30
1757  ]
1758  )
1759  : $callback();
1760 
1761  $sidebar['TOOLBOX'] = $this->makeToolbox(
1762  $this->buildNavUrls(),
1763  $this->buildFeedUrls()
1764  );
1765  $sidebar['LANGUAGES'] = $this->getLanguages();
1766  // Apply post-processing to the cached value
1767  $this->getHookRunner()->onSidebarBeforeOutput( $this, $sidebar );
1768 
1769  return $sidebar;
1770  }
1771 
1781  public function addToSidebar( &$bar, $message ) {
1782  $this->addToSidebarPlain( $bar, $this->msg( $message )->inContentLanguage()->plain() );
1783  }
1784 
1792  public function addToSidebarPlain( &$bar, $text ) {
1793  $lines = explode( "\n", $text );
1794 
1795  $heading = '';
1796  $config = $this->getConfig();
1797  $messageTitle = $config->get( 'EnableSidebarCache' )
1798  ? Title::newMainPage() : $this->getTitle();
1799  $messageCache = MediaWikiServices::getInstance()->getMessageCache();
1800 
1801  foreach ( $lines as $line ) {
1802  if ( strpos( $line, '*' ) !== 0 ) {
1803  continue;
1804  }
1805  $line = rtrim( $line, "\r" ); // for Windows compat
1806 
1807  if ( strpos( $line, '**' ) !== 0 ) {
1808  $heading = trim( $line, '* ' );
1809  if ( !array_key_exists( $heading, $bar ) ) {
1810  $bar[$heading] = [];
1811  }
1812  } else {
1813  $line = trim( $line, '* ' );
1814 
1815  if ( strpos( $line, '|' ) !== false ) { // sanity check
1816  $line = $messageCache->transform( $line, false, null, $messageTitle );
1817  $line = array_map( 'trim', explode( '|', $line, 2 ) );
1818  if ( count( $line ) !== 2 ) {
1819  // Second sanity check, could be hit by people doing
1820  // funky stuff with parserfuncs... (T35321)
1821  continue;
1822  }
1823 
1824  $extraAttribs = [];
1825 
1826  $msgLink = $this->msg( $line[0] )->title( $messageTitle )->inContentLanguage();
1827  if ( $msgLink->exists() ) {
1828  $link = $msgLink->text();
1829  if ( $link == '-' ) {
1830  continue;
1831  }
1832  } else {
1833  $link = $line[0];
1834  }
1835  $msgText = $this->msg( $line[1] )->title( $messageTitle );
1836  if ( $msgText->exists() ) {
1837  $text = $msgText->text();
1838  } else {
1839  $text = $line[1];
1840  }
1841 
1842  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
1843  $href = $link;
1844 
1845  // Parser::getExternalLinkAttribs won't work here because of the Namespace things
1846  if ( $config->get( 'NoFollowLinks' ) &&
1847  !wfMatchesDomainList( $href, $config->get( 'NoFollowDomainExceptions' ) )
1848  ) {
1849  $extraAttribs['rel'] = 'nofollow';
1850  }
1851 
1852  if ( $config->get( 'ExternalLinkTarget' ) ) {
1853  $extraAttribs['target'] = $config->get( 'ExternalLinkTarget' );
1854  }
1855  } else {
1856  $title = Title::newFromText( $link );
1857 
1858  if ( $title ) {
1859  $title = $title->fixSpecialName();
1860  $href = $title->getLinkURL();
1861  } else {
1862  $href = 'INVALID-TITLE';
1863  }
1864  }
1865 
1866  $bar[$heading][] = array_merge( [
1867  'text' => $text,
1868  'href' => $href,
1869  'id' => Sanitizer::escapeIdForAttribute( 'n-' . strtr( $line[1], ' ', '-' ) ),
1870  'active' => false,
1871  ], $extraAttribs );
1872  } else {
1873  continue;
1874  }
1875  }
1876  }
1877 
1878  return $bar;
1879  }
1880 
1886  public function getNewtalks() {
1887  $newMessagesAlert = '';
1888  $user = $this->getUser();
1889  $services = MediaWikiServices::getInstance();
1890  $linkRenderer = $services->getLinkRenderer();
1891  $userHasNewMessages = $services->getTalkPageNotificationManager()
1892  ->userHasNewMessages( $user );
1893  $timestamp = $services->getTalkPageNotificationManager()
1894  ->getLatestSeenMessageTimestamp( $user );
1895  $newtalks = !$userHasNewMessages ? [] : [
1896  [
1897  // TODO: Deprecate adding wiki and link to array and redesign GetNewMessagesAlert hook
1898  'wiki' => WikiMap::getCurrentWikiId(),
1899  'link' => $user->getTalkPage()->getLocalURL(),
1900  'rev' => $timestamp ? $services->getRevisionLookup()
1901  ->getRevisionByTimestamp( $user->getTalkPage(), $timestamp ) : null
1902  ]
1903  ];
1904  $out = $this->getOutput();
1905 
1906  // Allow extensions to disable or modify the new messages alert
1907  if ( !$this->getHookRunner()->onGetNewMessagesAlert(
1908  $newMessagesAlert, $newtalks, $user, $out )
1909  ) {
1910  return '';
1911  }
1912  if ( $newMessagesAlert ) {
1913  return $newMessagesAlert;
1914  }
1915 
1916  if ( $newtalks !== [] ) {
1917  $uTalkTitle = $user->getTalkPage();
1918  $lastSeenRev = $newtalks[0]['rev'];
1919  $numAuthors = 0;
1920  if ( $lastSeenRev !== null ) {
1921  $plural = true; // Default if we have a last seen revision: if unknown, use plural
1922  $revStore = $services->getRevisionStore();
1923  $latestRev = $revStore->getRevisionByTitle(
1924  $uTalkTitle,
1925  0,
1926  RevisionLookup::READ_NORMAL
1927  );
1928  if ( $latestRev !== null ) {
1929  // Singular if only 1 unseen revision, plural if several unseen revisions.
1930  $plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1931  $numAuthors = $revStore->countAuthorsBetween(
1932  $uTalkTitle->getArticleID(),
1933  $lastSeenRev,
1934  $latestRev,
1935  null,
1936  10,
1937  RevisionStore::INCLUDE_NEW
1938  );
1939  }
1940  } else {
1941  // Singular if no revision -> diff link will show latest change only in any case
1942  $plural = false;
1943  }
1944  $plural = $plural ? 999 : 1;
1945  // 999 signifies "more than one revision". We don't know how many, and even if we did,
1946  // the number of revisions or authors is not necessarily the same as the number of
1947  // "messages".
1948  $newMessagesLink = $linkRenderer->makeKnownLink(
1949  $uTalkTitle,
1950  $this->msg( 'newmessageslinkplural' )->params( $plural )->text(),
1951  [],
1952  $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
1953  );
1954 
1955  $newMessagesDiffLink = $linkRenderer->makeKnownLink(
1956  $uTalkTitle,
1957  $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->text(),
1958  [],
1959  $lastSeenRev !== null
1960  ? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1961  : [ 'diff' => 'cur' ]
1962  );
1963 
1964  if ( $numAuthors >= 1 && $numAuthors <= 10 ) {
1965  $newMessagesAlert = $this->msg(
1966  'youhavenewmessagesfromusers',
1967  $newMessagesLink,
1968  $newMessagesDiffLink
1969  )->numParams( $numAuthors, $plural );
1970  } else {
1971  // $numAuthors === 11 signifies "11 or more" ("more than 10")
1972  $newMessagesAlert = $this->msg(
1973  $numAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
1974  $newMessagesLink,
1975  $newMessagesDiffLink
1976  )->numParams( $plural );
1977  }
1978  $newMessagesAlert = $newMessagesAlert->text();
1979  // Disable CDN cache
1980  $out->setCdnMaxage( 0 );
1981  }
1982 
1983  return $newMessagesAlert;
1984  }
1985 
1993  private function getCachedNotice( $name ) {
1994  $config = $this->getConfig();
1995 
1996  if ( $name === 'default' ) {
1997  // special case
1998  $notice = $config->get( 'SiteNotice' );
1999  if ( empty( $notice ) ) {
2000  return false;
2001  }
2002  } else {
2003  $msg = $this->msg( $name )->inContentLanguage();
2004  if ( $msg->isBlank() ) {
2005  return '';
2006  } elseif ( $msg->isDisabled() ) {
2007  return false;
2008  }
2009  $notice = $msg->plain();
2010  }
2011 
2012  $services = MediaWikiServices::getInstance();
2013  $cache = $services->getMainWANObjectCache();
2014  $parsed = $cache->getWithSetCallback(
2015  // Use the extra hash appender to let eg SSL variants separately cache
2016  // Key is verified with md5 hash of unparsed wikitext
2017  $cache->makeKey( $name, $config->get( 'RenderHashAppend' ), md5( $notice ) ),
2018  // TTL in seconds
2019  600,
2020  function () use ( $notice ) {
2021  return $this->getOutput()->parseAsInterface( $notice );
2022  }
2023  );
2024 
2025  $contLang = $services->getContentLanguage();
2026  return Html::rawElement(
2027  'div',
2028  [
2029  'id' => 'localNotice',
2030  'lang' => $contLang->getHtmlCode(),
2031  'dir' => $contLang->getDir()
2032  ],
2033  $parsed
2034  );
2035  }
2036 
2040  public function getSiteNotice() {
2041  $siteNotice = '';
2042 
2043  if ( $this->getHookRunner()->onSiteNoticeBefore( $siteNotice, $this ) ) {
2044  if ( $this->getUser()->isRegistered() ) {
2045  $siteNotice = $this->getCachedNotice( 'sitenotice' );
2046  } else {
2047  $anonNotice = $this->getCachedNotice( 'anonnotice' );
2048  if ( $anonNotice === false ) {
2049  $siteNotice = $this->getCachedNotice( 'sitenotice' );
2050  } else {
2051  $siteNotice = $anonNotice;
2052  }
2053  }
2054  if ( $siteNotice === false ) {
2055  $siteNotice = $this->getCachedNotice( 'default' ) ?: '';
2056  }
2057  }
2058 
2059  $this->getHookRunner()->onSiteNoticeAfter( $siteNotice, $this );
2060  return $siteNotice;
2061  }
2062 
2075  public function doEditSectionLink( Title $nt, $section, $tooltip, Language $lang ) {
2076  // HTML generated here should probably have userlangattributes
2077  // added to it for LTR text on RTL pages
2078 
2079  $attribs = [];
2080  if ( $tooltip !== null ) {
2081  $attribs['title'] = $this->msg( 'editsectionhint' )->rawParams( $tooltip )
2082  ->inLanguage( $lang )->text();
2083  }
2084 
2085  $links = [
2086  'editsection' => [
2087  'text' => $this->msg( 'editsection' )->inLanguage( $lang )->text(),
2088  'targetTitle' => $nt,
2089  'attribs' => $attribs,
2090  'query' => [ 'action' => 'edit', 'section' => $section ]
2091  ]
2092  ];
2093 
2094  $this->getHookRunner()->onSkinEditSectionLinks( $this, $nt, $section, $tooltip, $links, $lang );
2095 
2096  $result = '<span class="mw-editsection"><span class="mw-editsection-bracket">[</span>';
2097 
2098  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
2099  $linksHtml = [];
2100  foreach ( $links as $k => $linkDetails ) {
2101  $linksHtml[] = $linkRenderer->makeKnownLink(
2102  $linkDetails['targetTitle'],
2103  $linkDetails['text'],
2104  $linkDetails['attribs'],
2105  $linkDetails['query']
2106  );
2107  }
2108 
2109  $result .= implode(
2110  '<span class="mw-editsection-divider">'
2111  . $this->msg( 'pipe-separator' )->inLanguage( $lang )->escaped()
2112  . '</span>',
2113  $linksHtml
2114  );
2115 
2116  $result .= '<span class="mw-editsection-bracket">]</span></span>';
2117  return $result;
2118  }
2119 
2129  public function makeToolbox( $navUrls, $feedUrls ) {
2130  $toolbox = [];
2131  if ( $navUrls['whatlinkshere'] ?? null ) {
2132  $toolbox['whatlinkshere'] = $navUrls['whatlinkshere'];
2133  $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
2134  }
2135  if ( $navUrls['recentchangeslinked'] ?? null ) {
2136  $toolbox['recentchangeslinked'] = $navUrls['recentchangeslinked'];
2137  $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
2138  $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
2139  $toolbox['recentchangeslinked']['rel'] = 'nofollow';
2140  }
2141  if ( $feedUrls ) {
2142  $toolbox['feeds']['id'] = 'feedlinks';
2143  $toolbox['feeds']['links'] = [];
2144  foreach ( $feedUrls as $key => $feed ) {
2145  $toolbox['feeds']['links'][$key] = $feed;
2146  $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
2147  $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
2148  $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
2149  $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
2150  }
2151  }
2152  foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
2153  'userrights', 'upload', 'specialpages' ] as $special
2154  ) {
2155  if ( $navUrls[$special] ?? null ) {
2156  $toolbox[$special] = $navUrls[$special];
2157  $toolbox[$special]['id'] = "t-$special";
2158  }
2159  }
2160  if ( $navUrls['print'] ?? null ) {
2161  $toolbox['print'] = $navUrls['print'];
2162  $toolbox['print']['id'] = 't-print';
2163  $toolbox['print']['rel'] = 'alternate';
2164  $toolbox['print']['msg'] = 'printableversion';
2165  }
2166  if ( $navUrls['permalink'] ?? null ) {
2167  $toolbox['permalink'] = $navUrls['permalink'];
2168  $toolbox['permalink']['id'] = 't-permalink';
2169  }
2170  if ( $navUrls['info'] ?? null ) {
2171  $toolbox['info'] = $navUrls['info'];
2172  $toolbox['info']['id'] = 't-info';
2173  }
2174 
2175  return $toolbox;
2176  }
2177 
2195  final public function getIndicatorsHTML( $indicators ) {
2196  wfDeprecated( __METHOD__, '1.36' );
2197 
2198  $out = "<div class=\"mw-indicators mw-body-content\">\n";
2199  foreach ( $this->getIndicatorsData( $indicators ) as $indicatorData ) {
2200  $out .= Html::rawElement(
2201  'div',
2202  [
2203  'id' => $indicatorData['id'],
2204  'class' => $indicatorData['class']
2205  ],
2206  $indicatorData['html']
2207  ) . "\n";
2208  }
2209  $out .= "</div>\n";
2210  return $out;
2211  }
2212 
2219  protected function getIndicatorsData( $indicators ) {
2220  $indicatorData = [];
2221  foreach ( $indicators as $id => $content ) {
2222  $indicatorData[] = [
2223  'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
2224  'class' => 'mw-indicator',
2225  'html' => $content,
2226  ];
2227  }
2228  return $indicatorData;
2229  }
2230 
2243  final public function getPersonalToolsForMakeListItem( $urls ) {
2244  $personal_tools = [];
2245  foreach ( $urls as $key => $plink ) {
2246  # The class on a personal_urls item is meant to go on the <a> instead
2247  # of the <li> so we have to use a single item "links" array instead
2248  # of using most of the personal_url's keys directly.
2249  $ptool = [
2250  'links' => [
2251  [ 'single-id' => "pt-$key" ],
2252  ],
2253  'id' => "pt-$key",
2254  ];
2255  if ( isset( $plink['active'] ) ) {
2256  $ptool['active'] = $plink['active'];
2257  }
2258  // Set class for the link to link-class, when defined.
2259  // This allows newer notifications content navigation to retain their classes
2260  // when merged back into the personal tools.
2261  // Doing this here allows the loop below to overwrite the class if defined directly.
2262  if ( isset( $plink['link-class'] ) ) {
2263  $ptool['links'][0]['class'] = $plink['link-class'];
2264  }
2265  foreach ( [
2266  'href',
2267  'class',
2268  'text',
2269  'dir',
2270  'data',
2271  'exists',
2272  'data-mw'
2273  ] as $k ) {
2274  if ( isset( $plink[$k] ) ) {
2275  $ptool['links'][0][$k] = $plink[$k];
2276  }
2277  }
2278  $personal_tools[$key] = $ptool;
2279  }
2280  return $personal_tools;
2281  }
2282 
2339  final public function makeLink( $key, $item, $options = [] ) {
2340  $text = $item['text'] ?? $this->msg( $item['msg'] ?? $key )->text();
2341 
2342  $html = htmlspecialchars( $text );
2343 
2344  if ( isset( $options['text-wrapper'] ) ) {
2345  $wrapper = $options['text-wrapper'];
2346  if ( isset( $wrapper['tag'] ) ) {
2347  $wrapper = [ $wrapper ];
2348  }
2349  while ( count( $wrapper ) > 0 ) {
2350  $element = array_pop( $wrapper );
2351  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
2352  $html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
2353  }
2354  }
2355 
2356  if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
2357  $attrs = $item;
2358  foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
2359  'tooltip-params', 'exists' ] as $k ) {
2360  unset( $attrs[$k] );
2361  }
2362 
2363  if ( isset( $attrs['data'] ) ) {
2364  foreach ( $attrs['data'] as $key => $value ) {
2365  $attrs[ 'data-' . $key ] = $value;
2366  }
2367  unset( $attrs[ 'data' ] );
2368  }
2369 
2370  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2371  $item['single-id'] = $item['id'];
2372  }
2373 
2374  $tooltipParams = [];
2375  if ( isset( $item['tooltip-params'] ) ) {
2376  $tooltipParams = $item['tooltip-params'];
2377  }
2378 
2379  if ( isset( $item['single-id'] ) ) {
2380  $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
2381 
2382  if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
2383  $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
2384  if ( $title !== false ) {
2385  $attrs['title'] = $title;
2386  }
2387  } else {
2389  $item['single-id'],
2390  $tooltipParams,
2391  $tooltipOption
2392  );
2393  if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
2394  $attrs['title'] = $tip['title'];
2395  }
2396  if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
2397  $attrs['accesskey'] = $tip['accesskey'];
2398  }
2399  }
2400  }
2401  if ( isset( $options['link-class'] ) ) {
2402  if ( isset( $attrs['class'] ) ) {
2403  // In the future, this should accept an array of classes, not a string
2404  if ( is_array( $attrs['class'] ) ) {
2405  $attrs['class'][] = $options['link-class'];
2406  } else {
2407  $attrs['class'] .= " {$options['link-class']}";
2408  }
2409  } else {
2410  $attrs['class'] = $options['link-class'];
2411  }
2412  }
2413  $html = Html::rawElement( isset( $attrs['href'] )
2414  ? 'a'
2415  : $options['link-fallback'], $attrs, $html );
2416  }
2417 
2418  return $html;
2419  }
2420 
2455  final public function makeListItem( $key, $item, $options = [] ) {
2456  // In case this is still set from SkinTemplate, we don't want it to appear in
2457  // the HTML output (normally removed in SkinTemplate::buildContentActionUrls())
2458  unset( $item['redundant'] );
2459 
2460  if ( isset( $item['links'] ) ) {
2461  $links = [];
2462  foreach ( $item['links'] as $linkKey => $link ) {
2463  $links[] = $this->makeLink( $linkKey, $link, $options );
2464  }
2465  $html = implode( ' ', $links );
2466  } else {
2467  $link = $item;
2468  // These keys are used by makeListItem and shouldn't be passed on to the link
2469  foreach ( [ 'id', 'class', 'active', 'tag', 'itemtitle' ] as $k ) {
2470  unset( $link[$k] );
2471  }
2472  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2473  // The id goes on the <li> not on the <a> for single links
2474  // but makeSidebarLink still needs to know what id to use when
2475  // generating tooltips and accesskeys.
2476  $link['single-id'] = $item['id'];
2477  }
2478  if ( isset( $link['link-class'] ) ) {
2479  // link-class should be set on the <a> itself,
2480  // so pass it in as 'class'
2481  $link['class'] = $link['link-class'];
2482  unset( $link['link-class'] );
2483  }
2484  $html = $this->makeLink( $key, $link, $options );
2485  }
2486 
2487  $attrs = [];
2488  foreach ( [ 'id', 'class' ] as $attr ) {
2489  if ( isset( $item[$attr] ) ) {
2490  $attrs[$attr] = $item[$attr];
2491  }
2492  }
2493  if ( isset( $item['active'] ) && $item['active'] ) {
2494  if ( !isset( $attrs['class'] ) ) {
2495  $attrs['class'] = '';
2496  }
2497 
2498  // In the future, this should accept an array of classes, not a string
2499  if ( is_array( $attrs['class'] ) ) {
2500  $attrs['class'][] = 'active';
2501  } else {
2502  $attrs['class'] .= ' active';
2503  $attrs['class'] = trim( $attrs['class'] );
2504  }
2505  }
2506  if ( isset( $item['itemtitle'] ) ) {
2507  $attrs['title'] = $item['itemtitle'];
2508  }
2509  return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
2510  }
2511 
2518  final public function makeSearchInput( $attrs = [] ) {
2519  $autoCapHint = $this->getConfig()->get( 'CapitalLinks' );
2520  $realAttrs = [
2521  'type' => 'search',
2522  'name' => 'search',
2523  'placeholder' => $this->msg( 'searchsuggest-search' )->text(),
2524  // T251664: Disable autocapitalization of input
2525  // method when using fully case-sensitive titles.
2526  'autocapitalize' => $autoCapHint ? 'sentences' : 'none',
2527  ];
2528 
2529  $realAttrs = array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
2530  return Html::element( 'input', $realAttrs );
2531  }
2532 
2540  final public function makeSearchButton( $mode, $attrs = [] ) {
2541  switch ( $mode ) {
2542  case 'go':
2543  case 'fulltext':
2544  $realAttrs = [
2545  'type' => 'submit',
2546  'name' => $mode,
2547  'value' => $this->msg( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
2548  ];
2549  $realAttrs = array_merge(
2550  $realAttrs,
2551  Linker::tooltipAndAccesskeyAttribs( "search-$mode" ),
2552  $attrs
2553  );
2554  return Html::element( 'input', $realAttrs );
2555  case 'image':
2556  $buttonAttrs = [
2557  'type' => 'submit',
2558  'name' => 'button',
2559  ];
2560  $buttonAttrs = array_merge(
2561  $buttonAttrs,
2562  Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ),
2563  $attrs
2564  );
2565  unset( $buttonAttrs['src'] );
2566  unset( $buttonAttrs['alt'] );
2567  unset( $buttonAttrs['width'] );
2568  unset( $buttonAttrs['height'] );
2569  $imgAttrs = [
2570  'src' => $attrs['src'],
2571  'alt' => $attrs['alt'] ?? $this->msg( 'searchbutton' )->text(),
2572  'width' => $attrs['width'] ?? null,
2573  'height' => $attrs['height'] ?? null,
2574  ];
2575  return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
2576  default:
2577  throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' );
2578  }
2579  }
2580 
2591  public function getAfterPortlet( string $name ) : string {
2592  $html = '';
2593 
2594  $this->getHookRunner()->onSkinAfterPortlet( $this, $name, $html );
2595 
2596  return $html;
2597  }
2598 
2604  final public function prepareSubtitle() {
2605  $out = $this->getOutput();
2606  $subpagestr = $this->subPageSubtitleInternal();
2607  if ( $subpagestr !== '' ) {
2608  $subpagestr = '<span class="subpages">' . $subpagestr . '</span>';
2609  }
2610  return $subpagestr . $out->getSubtitle();
2611  }
2612 
2624  protected function getFooterLinks(): array {
2625  $out = $this->getOutput();
2626  $title = $out->getTitle();
2627  $titleExists = $title->exists();
2628  $config = $this->getConfig();
2629  $maxCredits = $config->get( 'MaxCredits' );
2630  $showCreditsIfMax = $config->get( 'ShowCreditsIfMax' );
2631  $useCredits = $titleExists
2632  && $out->isArticle()
2633  && $out->isRevisionCurrent()
2634  && $maxCredits !== 0;
2635 
2637  if ( $useCredits ) {
2638  $article = Article::newFromWikiPage( $this->getWikiPage(), $this );
2639  $action = Action::factory( 'credits', $article, $this );
2640  }
2641 
2642  '@phan-var CreditsAction $action';
2643  $data = [
2644  'info' => [
2645  'lastmod' => !$useCredits ? $this->lastModified() : null,
2646  'numberofwatchingusers' => null,
2647  'credits' => $useCredits ?
2648  $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
2649  'copyright' => $titleExists &&
2650  $out->showsCopyright() ? $this->getCopyright() : null,
2651  ],
2652  'places' => $this->getSiteFooterLinks(),
2653  ];
2654  foreach ( $data as $key => $existingItems ) {
2655  $newItems = [];
2656  $this->getHookRunner()->onSkinAddFooterLinks( $this, $key, $newItems );
2657  $data[$key] = $existingItems + $newItems;
2658  }
2659  return $data;
2660  }
2661 
2662 }
Skin\prepareSubtitle
prepareSubtitle()
Prepare the subtitle of the page for output in the skin if one has been set.
Definition: Skin.php:2604
OutputPage\addMeta
addMeta( $name, $val)
Add a new "<meta>" tag To add an http-equiv meta tag, precede the name with "http:".
Definition: OutputPage.php:405
Skin\$skinname
string null $skinname
Definition: Skin.php:48
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:71
Skin\editUrlOptions
editUrlOptions()
Return URL options for the 'edit page' link.
Definition: Skin.php:1237
Skin\showEmailUser
showEmailUser( $id)
Definition: Skin.php:1252
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:623
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:363
Skin\makeUrlDetails
static makeUrlDetails( $name, $urlaction='')
these return an array with the 'href' and boolean 'exists'
Definition: Skin.php:1383
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:46
Skin\getSiteNotice
getSiteNotice()
Definition: Skin.php:2040
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
ResourceLoader\makeConfigSetScript
static makeConfigSetScript(array $configuration)
Return JS code which will set the MediaWiki configuration array to the given value.
Definition: ResourceLoader.php:1671
Skin\VERSION_MAJOR
const VERSION_MAJOR
The current major version of the skin specification.
Definition: Skin.php:64
OutputPage\getDisplayTitle
getDisplayTitle()
Returns page display title.
Definition: OutputPage.php:1022
Skin\buildFeedUrls
buildFeedUrls()
Build data structure representing syndication links.
Definition: Skin.php:1687
Skin\subPageSubtitleInternal
subPageSubtitleInternal()
Definition: Skin.php:848
Skin\$options
array $options
Skin options passed into constructor.
Definition: Skin.php:53
Skin\footerLinkTitle
footerLinkTitle( $desc, $page)
Definition: Skin.php:1146
Skin\getPoweredBy
getPoweredBy()
Gets the powered by MediaWiki icon.
Definition: Skin.php:989
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:172
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:84
ResourceLoader\makeInlineScript
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
Definition: ResourceLoader.php:1644
Sanitizer\escapeIdForAttribute
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:812
true
return true
Definition: router.php:90
Skin\makeVariablesScript
static makeVariablesScript( $data, $nonce=null)
Definition: Skin.php:466
$fallback
$fallback
Definition: MessagesAb.php:11
Skin\lastModified
lastModified()
Get the timestamp of the latest revision, formatted in user language.
Definition: Skin.php:1012
Skin\getSiteFooterLinks
getSiteFooterLinks()
Gets the link to the wiki's privacy policy, about page, and disclaimer page.
Definition: Skin.php:1166
UploadBase\isEnabled
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:143
Skin\checkTitle
static checkTitle(&$title, $name)
make sure we have some title to operate on
Definition: Skin.php:1415
MWDebug\getHTMLDebugLog
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition: MWDebug.php:644
Skin\mapInterwikiToLanguage
mapInterwikiToLanguage( $code)
Allows correcting the language of interlanguage links which, mostly due to legacy reasons,...
Definition: Skin.php:1432
Skin\makeSpecialUrl
static makeSpecialUrl( $name, $urlaction='', $proto=null)
Make a URL for a Special Page using the given query and protocol.
Definition: Skin.php:1311
Skin\getCategoryLinks
getCategoryLinks()
Definition: Skin.php:567
Skin\aboutLink
aboutLink()
Gets the link to the wiki's about page.
Definition: Skin.php:1214
Skin\makeSpecialUrlSubpage
static makeSpecialUrlSubpage( $name, $subpage, $urlaction='')
Definition: Skin.php:1326
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:584
SpecialPage\getTitleFor
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,...
Definition: SpecialPage.php:107
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:973
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:80
Title\newMainPage
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:688
Skin\$mRelevantUser
$mRelevantUser
Definition: Skin.php:55
Action\factory
static factory(?string $action, Page $article, IContextSource $context=null)
Get an appropriate Action subclass for the given action.
Definition: Action.php:115
ContextSource\getUser
getUser()
Stable to override.
Definition: ContextSource.php:135
ContextSource\getTitle
getTitle()
Definition: ContextSource.php:89
wfExpandIRI
wfExpandIRI( $url)
Take a URL, make sure it's expanded to fully qualified, and replace any encoded non-ASCII Unicode cha...
Definition: GlobalFunctions.php:865
WikiMap\getCurrentWikiId
static getCurrentWikiId()
Definition: WikiMap.php:303
Skin\getHtmlElementAttributes
getHtmlElementAttributes()
Return values for <html> element.
Definition: Skin.php:544
SpecialEmailUser\validateTarget
static validateTarget( $target, User $sender)
Validate target User.
Definition: SpecialEmailUser.php:218
Skin\doEditSectionLink
doEditSectionLink(Title $nt, $section, $tooltip, Language $lang)
Create a section edit link.
Definition: Skin.php:2075
Skin\makeListItem
makeListItem( $key, $item, $options=[])
Generates a list item for a navigation, portlet, portal, sidebar...
Definition: Skin.php:2455
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Skin\doSetupSkinUserCss
doSetupSkinUserCss(OutputPage $out)
Call the subclass's setupSkinUserCss and throw a deprecation warning if required.
Definition: Skin.php:486
Skin\bottomScripts
bottomScripts()
This gets called shortly before the "</body>" tag.
Definition: Skin.php:739
Skin\mainPageLink
mainPageLink()
Gets the link to the wiki's main page.
Definition: Skin.php:1098
Linker\tooltipAndAccesskeyAttribs
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2284
Skin\getSkinNames
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:83
NS_MAIN
const NS_MAIN
Definition: Defines.php:64
Skin\afterContentHook
afterContentHook()
This runs a hook to allow extensions placing their stuff after content and article metadata (e....
Definition: Skin.php:701
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:151
SpecialPage\getSafeTitleFor
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
Definition: SpecialPage.php:136
Skin\subPageSubtitle
subPageSubtitle( $out)
Definition: Skin.php:840
Skin\getCachedNotice
getCachedNotice( $name)
Get a cached notice.
Definition: Skin.php:1993
UserrightsPage
Special page to allow managing user group membership.
Definition: SpecialUserrights.php:36
Article\newFromWikiPage
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:184
MWException
MediaWiki exception.
Definition: MWException.php:29
Skin\getIndicatorsData
getIndicatorsData( $indicators)
Return an array of indicator data.
Definition: Skin.php:2219
Skin\buildSidebar
buildSidebar()
Build an array that represents the sidebar(s), the navigation bar among them.
Definition: Skin.php:1726
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1034
Skin\privacyLink
privacyLink()
Gets the link to the wiki's privacy policy page.
Definition: Skin.php:1203
Skin\normalizeKey
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:115
Skin\footerLink
footerLink( $desc, $page)
Given a pair of message keys for link and text label, return an HTML link for use in the footer.
Definition: Skin.php:1127
Skin\generateDebugHTML
generateDebugHTML()
Generate debug data HTML for displaying at the bottom of the main content area.
Definition: Skin.php:729
$wgFallbackSkin
$wgFallbackSkin
Fallback skin used when the skin defined by $wgDefaultSkin can't be found.
Definition: DefaultSettings.php:3551
Skin\$stylename
string $stylename
Stylesheets set to use.
Definition: Skin.php:61
Skin\getCategories
getCategories()
Definition: Skin.php:666
Skin\setRelevantUser
setRelevantUser( $u)
Definition: Skin.php:418
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:125
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:32
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:116
$modules
$modules
Definition: HTMLFormElement.php:15
Skin\addToSidebar
addToSidebar(&$bar, $message)
Add content from a sidebar system message Currently only used for MediaWiki:Sidebar (but may be used ...
Definition: Skin.php:1781
User\isIP
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:1000
Skin\isRevisionCurrent
isRevisionCurrent()
Whether the revision displayed is the latest revision of the page.
Definition: Skin.php:386
Skin\drawCategoryBrowser
drawCategoryBrowser( $tree)
Render the array as a series of links.
Definition: Skin.php:642
Skin\getLanguages
getLanguages()
Generates array of language links for the current page.
Definition: Skin.php:1445
Skin\makeNSUrl
static makeNSUrl( $name, $urlaction='', $namespace=NS_MAIN)
this can be passed the NS number as defined in Language.php
Definition: Skin.php:1369
$title
$title
Definition: testCompression.php:38
Skin\preloadExistence
preloadExistence()
Preload the existence of three commonly-requested pages in a single query.
Definition: Skin.php:325
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:846
$wgDefaultSkin
$wgDefaultSkin
Default skin, for new users and anonymous visitors.
Definition: DefaultSettings.php:3544
Skin\disclaimerLink
disclaimerLink()
Gets the link to the wiki's general disclaimers page.
Definition: Skin.php:1225
Skin\getLogo
getLogo()
URL to the default square logo (1x key)
Definition: Skin.php:559
$revStore
$revStore
Definition: testCompression.php:55
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:915
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:47
Skin\makeToolbox
makeToolbox( $navUrls, $feedUrls)
Create an array of common toolbox items from the data in the quicktemplate stored by SkinTemplate.
Definition: Skin.php:2129
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:432
Skin\$mRelevantTitle
$mRelevantTitle
Definition: Skin.php:54
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:195
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:723
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:652
Skin\getRelevantTitle
getRelevantTitle()
Return the "relevant" title.
Definition: Skin.php:410
$content
$content
Definition: router.php:76
Skin\getSkinStylePath
getSkinStylePath( $name)
Return a fully resolved style path URL to images or styles stored in the current skin's folder.
Definition: Skin.php:1278
Skin\getPageClasses
getPageClasses( $title)
TODO: document.
Definition: Skin.php:509
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:188
Skin\makeSearchInput
makeSearchInput( $attrs=[])
Definition: Skin.php:2518
Skin\getDefaultModules
getDefaultModules()
Defines the ResourceLoader modules that should be added to the skin It is recommended that skins wish...
Definition: Skin.php:243
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:142
Skin\getIndicatorsHTML
getIndicatorsHTML( $indicators)
Get the suggested HTML for page status indicators: icons (or short text snippets) usually displayed i...
Definition: Skin.php:2195
Skin\getSearchLink
getSearchLink()
Definition: Skin.php:905
$line
$line
Definition: mcc.php:119
Skin\getPersonalToolsForMakeListItem
getPersonalToolsForMakeListItem( $urls)
Create an array of personal tools items from the data in the quicktemplate stored by SkinTemplate.
Definition: Skin.php:2243
Skin\printSource
printSource()
Text with the permalink to the source page, usually shown on the footer of a printed page.
Definition: Skin.php:762
UploadBase\isAllowed
static isAllowed(Authority $performer)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
Definition: UploadBase.php:157
NS_USER
const NS_USER
Definition: Defines.php:66
Skin\buildNavUrls
buildNavUrls()
Build array of common navigation links.
Definition: Skin.php:1551
Skin\getAfterPortlet
getAfterPortlet(string $name)
Allows extensions to hook into known portlets and add stuff to them.
Definition: Skin.php:2591
Skin\__construct
__construct( $options=null)
Definition: Skin.php:167
Linker\titleAttrib
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2105
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:45
Skin\logoText
logoText( $align='')
Definition: Skin.php:1046
Skin\getNewtalks
getNewtalks()
Gets new talk page messages for the current user and returns an appropriate alert message (or an empt...
Definition: Skin.php:1886
Skin\getVersion
static getVersion()
Get the current major version of Skin.
Definition: Skin.php:73
Title
Represents a title within MediaWiki.
Definition: Title.php:46
MWDebug\detectDeprecatedOverride
static detectDeprecatedOverride( $instance, $class, $method, $version=false, $component=false, $callerOffset=2)
Show a warning if $method declared in $class is overridden in $instance.
Definition: MWDebug.php:254
Skin\makeUrl
static makeUrl( $name, $urlaction='')
Definition: Skin.php:1337
wfMatchesDomainList
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
Definition: GlobalFunctions.php:881
$cache
$cache
Definition: mcc.php:33
LanguageCode\bcp47
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Definition: LanguageCode.php:175
Skin\getCopyright
getCopyright( $type='detect')
Definition: Skin.php:916
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
SpecialPage\setContext
setContext( $context)
Sets the context this SpecialPage is executed in.
Definition: SpecialPage.php:753
Skin\getRelevantUser
getRelevantUser()
Return the "relevant" user.
Definition: Skin.php:430
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
SkinException
Exceptions for skin-related failures.
Definition: SkinException.php:29
Skin\getUndeleteLink
getUndeleteLink()
Definition: Skin.php:781
Skin\makeKnownUrlDetails
static makeKnownUrlDetails( $name, $urlaction='')
Make URL details where the article exists (or at least it's convenient to think so)
Definition: Skin.php:1399
Skin\setupSkinUserCss
setupSkinUserCss(OutputPage $out)
Hook point for adding style modules to OutputPage.
Definition: Skin.php:500
CreditsAction
Definition: CreditsAction.php:31
Skin\makeMainPageUrl
static makeMainPageUrl( $urlaction='')
Definition: Skin.php:1293
Skin\getFooterLinks
getFooterLinks()
Get template representation of the footer containing site footer links as well as standard footer lin...
Definition: Skin.php:2624
$t
$t
Definition: testCompression.php:74
Skin\getCopyrightIcon
getCopyrightIcon()
Definition: Skin.php:959
Skin\outputPage
outputPage()
Outputs the HTML generated by other functions.
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:234
Skin\getSkinName
getSkinName()
Definition: Skin.php:185
Skin\getAllowedSkins
static getAllowedSkins()
Fetch the list of user-selectable skins in regards to $wgSkipSkins.
Definition: Skin.php:99
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:42
Skin\initPage
initPage(OutputPage $out)
Stable to override.
Definition: Skin.php:206
Skin\setRelevantTitle
setRelevantTitle( $t)
Definition: Skin.php:396
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
Skin\isResponsive
isResponsive()
Indicates if this skin is responsive.
Definition: Skin.php:198
Skin\makeSearchButton
makeSearchButton( $mode, $attrs=[])
Definition: Skin.php:2540
Skin\getRevisionId
getRevisionId()
Get the current revision ID.
Definition: Skin.php:374
Skin\addToSidebarPlain
addToSidebarPlain(&$bar, $text)
Add content from plain text.
Definition: Skin.php:1792
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
Skin\makeLink
makeLink( $key, $item, $options=[])
Makes a link, usually used by makeListItem to generate a link for an item in a list used in navigatio...
Definition: Skin.php:2339
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1351
Skin\makeFooterIcon
makeFooterIcon( $icon, $withImage='withImage')
Renders a $wgFooterIcons icon according to the method's arguments.
Definition: Skin.php:1069
$type
$type
Definition: testCompression.php:52