MediaWiki  master
Skin.php
Go to the documentation of this file.
1 <?php
23 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
29 use Wikimedia\WrappedString;
30 use Wikimedia\WrappedStringList;
31 
44 abstract class Skin extends ContextSource {
45  use ProtectedHookAccessorTrait;
46 
50  private $defaultLinkOptions = [];
51 
55  protected $skinname = null;
56 
60  protected $options = [];
61  protected $mRelevantTitle = null;
62 
66  private $mRelevantUser = false;
67 
72  public $stylename = null;
73 
75  protected const VERSION_MAJOR = 1;
76 
77  private $searchPageTitle = null;
78 
86  public static function getVersion() {
87  return self::VERSION_MAJOR;
88  }
89 
96  public static function getSkinNames() {
97  wfDeprecated( __METHOD__, '1.36' );
98 
99  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
100  return $skinFactory->getSkinNames();
101  }
102 
112  public static function getAllowedSkins() {
113  wfDeprecated( __METHOD__, '1.36' );
114 
115  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
116  return $skinFactory->getAllowedSkins();
117  }
118 
128  public static function normalizeKey( $key ) {
130 
131  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
132  $skinNames = $skinFactory->getSkinNames();
133 
134  // Make keys lowercase for case-insensitive matching.
135  $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
136  $key = strtolower( $key );
137  $defaultSkin = strtolower( $wgDefaultSkin );
138  $fallbackSkin = strtolower( $wgFallbackSkin );
139 
140  if ( $key == '' || $key == 'default' ) {
141  // Don't return the default immediately;
142  // in a misconfiguration we need to fall back.
143  $key = $defaultSkin;
144  }
145 
146  if ( isset( $skinNames[$key] ) ) {
147  return $key;
148  }
149 
150  // Older versions of the software used a numeric setting
151  // in the user preferences.
152  $fallback = [
153  0 => $defaultSkin,
154  2 => 'cologneblue'
155  ];
156 
157  if ( isset( $fallback[$key] ) ) {
158  $key = $fallback[$key];
159  }
160 
161  if ( isset( $skinNames[$key] ) ) {
162  return $key;
163  } elseif ( isset( $skinNames[$defaultSkin] ) ) {
164  return $defaultSkin;
165  } else {
166  return $fallbackSkin;
167  }
168  }
169 
183  public function __construct( $options = null ) {
184  if ( is_string( $options ) ) {
185  $this->skinname = $options;
186  } elseif ( $options ) {
187  $name = $options['name'] ?? null;
188 
189  if ( !$name ) {
190  throw new SkinException( 'Skin name must be specified' );
191  }
192 
193  if ( isset( $options['link'] ) ) {
194  $this->defaultLinkOptions = $options['link'];
195  }
196  $this->options = $options;
197  $this->skinname = $name;
198  }
199  }
200 
204  public function getSkinName() {
205  return $this->skinname;
206  }
207 
217  public function isResponsive() {
218  $isSkinResponsiveCapable = $this->options['responsive'] ?? false;
219  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
220 
221  return $isSkinResponsiveCapable &&
222  $userOptionsLookup->getBoolOption( $this->getUser(), 'skin-responsive' );
223  }
224 
229  public function initPage( OutputPage $out ) {
230  $skinMetaTags = $this->getConfig()->get( 'SkinMetaTags' );
231  $this->preloadExistence();
232 
233  if ( $this->isResponsive() ) {
234  $out->addMeta(
235  'viewport',
236  'width=device-width, initial-scale=1.0, ' .
237  'user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0'
238  );
239  }
240 
241  $tags = [
242  'og:title' => $out->getHTMLTitle(),
243  'twitter:card' => 'summary_large_image',
244  'og:type' => 'website',
245  ];
246 
247  // Support sharing on platforms such as Facebook and Twitter
248  foreach ( $tags as $key => $value ) {
249  if ( in_array( $key, $skinMetaTags ) ) {
250  $out->addMeta( $key, $value );
251  }
252  }
253  }
254 
266  public function getDefaultModules() {
267  $out = $this->getOutput();
268  $user = $this->getUser();
269 
270  // Modules declared in the $modules literal are loaded
271  // for ALL users, on ALL pages, in ALL skins.
272  // Keep this list as small as possible!
273  $modules = [
274  // The 'styles' key sets render-blocking style modules
275  // Unlike other keys in $modules, this is an associative array
276  // where each key is its own group pointing to a list of modules
277  'styles' => [
278  'skin' => $this->options['styles'] ?? [],
279  'core' => [],
280  'content' => [],
281  'syndicate' => [],
282  ],
283  'core' => [
284  'site',
285  'mediawiki.page.ready',
286  ],
287  // modules that enhance the content in some way
288  'content' => [],
289  // modules relating to search functionality
290  'search' => [],
291  // Skins can register their own scripts
292  'skin' => $this->options['scripts'] ?? [],
293  // modules relating to functionality relating to watching an article
294  'watch' => [],
295  // modules which relate to the current users preferences
296  'user' => [],
297  // modules relating to RSS/Atom Feeds
298  'syndicate' => [],
299  ];
300 
301  // Preload jquery.tablesorter for mediawiki.page.ready
302  if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
303  $modules['content'][] = 'jquery.tablesorter';
304  $modules['styles']['content'][] = 'jquery.tablesorter.styles';
305  }
306 
307  // Preload jquery.makeCollapsible for mediawiki.page.ready
308  if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
309  $modules['content'][] = 'jquery.makeCollapsible';
310  $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
311  }
312 
313  // Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
314  // on every page is deprecated. Express a dependency instead.
315  if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
316  $modules['styles']['content'][] = 'mediawiki.ui.button';
317  }
318 
319  if ( $out->isTOCEnabled() ) {
320  $modules['content'][] = 'mediawiki.toc';
321  }
322 
323  $authority = $this->getAuthority();
324  if ( $authority->getUser()->isRegistered()
325  && $authority->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
326  && $this->getRelevantTitle()->canExist()
327  ) {
328  $modules['watch'][] = 'mediawiki.page.watch.ajax';
329  }
330 
331  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
332  if ( $userOptionsLookup->getBoolOption( $user, 'editsectiononrightclick' )
333  || ( $out->isArticle() && $userOptionsLookup->getOption( $user, 'editondblclick' ) )
334  ) {
335  $modules['user'][] = 'mediawiki.misc-authed-pref';
336  }
337 
338  if ( $out->isSyndicated() ) {
339  $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
340  }
341 
342  return $modules;
343  }
344 
348  protected function preloadExistence() {
349  $titles = [];
350 
351  // User/talk link
352  $user = $this->getUser();
353  if ( $user->isRegistered() ) {
354  $titles[] = $user->getUserPage();
355  $titles[] = $user->getTalkPage();
356  }
357 
358  // Check, if the page can hold some kind of content, otherwise do nothing
359  $title = $this->getRelevantTitle();
360  if ( $title->canExist() && $title->canHaveTalkPage() ) {
361  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
362  if ( $title->isTalkPage() ) {
363  $titles[] = $namespaceInfo->getSubjectPage( $title );
364  } else {
365  $titles[] = $namespaceInfo->getTalkPage( $title );
366  }
367  }
368 
369  // Footer links (used by SkinTemplate::prepareQuickTemplate)
370  if ( $this->getConfig()->get( 'FooterLinkCacheExpiry' ) <= 0 ) {
371  $titles = array_merge(
372  $titles,
373  array_filter( [
374  $this->footerLinkTitle( 'privacy', 'privacypage' ),
375  $this->footerLinkTitle( 'aboutsite', 'aboutpage' ),
376  $this->footerLinkTitle( 'disclaimers', 'disclaimerpage' ),
377  ] )
378  );
379  }
380 
381  $this->getHookRunner()->onSkinPreloadExistence( $titles, $this );
382 
383  if ( $titles ) {
384  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
385  $lb = $linkBatchFactory->newLinkBatch( $titles );
386  $lb->setCaller( __METHOD__ );
387  $lb->execute();
388  }
389  }
390 
395  public function setRelevantTitle( $t ) {
396  $this->mRelevantTitle = $t;
397  }
398 
409  public function getRelevantTitle() {
410  return $this->mRelevantTitle ?? $this->getTitle();
411  }
412 
417  public function setRelevantUser( ?UserIdentity $u ) {
418  $this->mRelevantUser = $u;
419  }
420 
430  public function getRelevantUser(): ?UserIdentity {
431  if ( $this->mRelevantUser === false ) {
432  $this->mRelevantUser = null; // false indicates we never attempted to load it.
433  $title = $this->getRelevantTitle();
434  if ( $title->hasSubjectNamespace( NS_USER ) ) {
435  $services = MediaWikiServices::getInstance();
436  $rootUser = $title->getRootText();
437  $userNameUtils = $services->getUserNameUtils();
438  if ( $userNameUtils->isIP( $rootUser ) ) {
439  $this->mRelevantUser = UserIdentityValue::newAnonymous( $rootUser );
440  } else {
441  $user = $services->getUserIdentityLookup()->getUserIdentityByName( $rootUser );
442  $this->mRelevantUser = $user && $user->isRegistered() ? $user : null;
443  }
444  }
445  }
446 
447  // The relevant user should only be set if it exists. However, if it exists but is hidden,
448  // and the viewer cannot see hidden users, this exposes the fact that the user exists;
449  // pretend like the user does not exist in such cases, by setting it to null. T120883
450  if ( $this->mRelevantUser && $this->mRelevantUser->isRegistered() ) {
451  $userBlock = MediaWikiServices::getInstance()
452  ->getBlockManager()
453  ->getUserBlock( $this->mRelevantUser, null, true );
454  if ( $userBlock && $userBlock->getHideName() &&
455  !$this->getAuthority()->isAllowed( 'hideuser' )
456  ) {
457  $this->mRelevantUser = null;
458  }
459  }
460 
461  return $this->mRelevantUser;
462  }
463 
467  abstract public function outputPage();
468 
475  public static function makeVariablesScript( $data, $nonce = null ) {
476  wfDeprecated( __METHOD__, '1.36' );
477 
478  if ( $data ) {
481  $nonce
482  );
483  }
484  return '';
485  }
486 
492  public function getPageClasses( $title ) {
493  $numeric = 'ns-' . $title->getNamespace();
494 
495  if ( $title->isSpecialPage() ) {
496  $type = 'ns-special';
497  // T25315: provide a class based on the canonical special page name without subpages
498  list( $canonicalName ) = MediaWikiServices::getInstance()->getSpecialPageFactory()->
499  resolveAlias( $title->getDBkey() );
500  if ( $canonicalName ) {
501  $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
502  } else {
503  $type .= ' mw-invalidspecialpage';
504  }
505  } else {
506  if ( $title->isTalkPage() ) {
507  $type = 'ns-talk';
508  } else {
509  $type = 'ns-subject';
510  }
511  // T208315: add HTML class when the user can edit the page
512  if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
513  $type .= ' mw-editable';
514  }
515  }
516 
517  $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() );
518  $root = Sanitizer::escapeClass( 'rootpage-' . $title->getRootTitle()->getPrefixedText() );
519 
520  return "$numeric $type $name $root";
521  }
522 
527  public function getHtmlElementAttributes() {
528  $lang = $this->getLanguage();
529  return [
530  'lang' => $lang->getHtmlCode(),
531  'dir' => $lang->getDir(),
532  'class' => 'client-nojs',
533  ];
534  }
535 
542  protected function getLogo() {
543  wfDeprecated( __METHOD__, '1.36' );
544  return ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() )[ '1x' ];
545  }
546 
550  public function getCategoryLinks() {
551  $out = $this->getOutput();
552  $allCats = $out->getCategoryLinks();
553  $title = $this->getTitle();
554  $services = MediaWikiServices::getInstance();
555  $linkRenderer = $services->getLinkRenderer();
556 
557  if ( $allCats === [] ) {
558  return '';
559  }
560 
561  $embed = "<li>";
562  $pop = "</li>";
563 
564  $s = '';
565  $colon = $this->msg( 'colon-separator' )->escaped();
566 
567  if ( !empty( $allCats['normal'] ) ) {
568  $t = $embed . implode( $pop . $embed, $allCats['normal'] ) . $pop;
569 
570  $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) );
571  $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
572  $pageCategoriesLinkTitle = Title::newFromText( $linkPage );
573  if ( $pageCategoriesLinkTitle ) {
574  $link = $linkRenderer->makeLink( $pageCategoriesLinkTitle, $msg->text() );
575  } else {
576  $link = $msg->escaped();
577  }
578  $s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' .
579  $link . $colon . '<ul>' . $t . '</ul></div>';
580  }
581 
582  # Hidden categories
583  if ( isset( $allCats['hidden'] ) ) {
584  $userOptionsLookup = $services->getUserOptionsLookup();
585 
586  if ( $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' ) ) {
587  $class = ' mw-hidden-cats-user-shown';
588  } elseif ( $title->inNamespace( NS_CATEGORY ) ) {
589  $class = ' mw-hidden-cats-ns-shown';
590  } else {
591  $class = ' mw-hidden-cats-hidden';
592  }
593 
594  $s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" .
595  $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
596  $colon . '<ul>' . $embed . implode( $pop . $embed, $allCats['hidden'] ) . $pop . '</ul>' .
597  '</div>';
598  }
599 
600  # optional 'dmoz-like' category browser. Will be shown under the list
601  # of categories an article belong to
602  if ( $this->getConfig()->get( 'UseCategoryBrowser' ) ) {
603  $s .= '<br /><hr />';
604 
605  # get a big array of the parents tree
606  $parenttree = $title->getParentCategoryTree();
607  # Skin object passed by reference cause it can not be
608  # accessed under the method subfunction drawCategoryBrowser
609  $tempout = explode( "\n", $this->drawCategoryBrowser( $parenttree ) );
610  # Clean out bogus first entry and sort them
611  unset( $tempout[0] );
612  asort( $tempout );
613  # Output one per line
614  $s .= implode( "<br />\n", $tempout );
615  }
616 
617  return $s;
618  }
619 
625  protected function drawCategoryBrowser( $tree ) {
626  $return = '';
627  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
628 
629  foreach ( $tree as $element => $parent ) {
630  if ( empty( $parent ) ) {
631  # element start a new list
632  $return .= "\n";
633  } else {
634  # grab the others elements
635  $return .= $this->drawCategoryBrowser( $parent ) . ' &gt; ';
636  }
637 
638  # add our current element to the list
639  $eltitle = Title::newFromText( $element );
640  $return .= $linkRenderer->makeLink( $eltitle, $eltitle->getText() );
641  }
642 
643  return $return;
644  }
645 
649  public function getCategories() {
650  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
651  $showHiddenCats = $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' );
652 
653  $catlinks = $this->getCategoryLinks();
654  // Check what we're showing
655  $allCats = $this->getOutput()->getCategoryLinks();
656  $showHidden = $showHiddenCats || $this->getTitle()->inNamespace( NS_CATEGORY );
657 
658  $classes = [ 'catlinks' ];
659  if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
660  $classes[] = 'catlinks-allhidden';
661  }
662 
663  return Html::rawElement(
664  'div',
665  [ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
666  $catlinks
667  );
668  }
669 
684  protected function afterContentHook() {
685  $data = '';
686 
687  if ( $this->getHookRunner()->onSkinAfterContent( $data, $this ) ) {
688  // adding just some spaces shouldn't toggle the output
689  // of the whole <div/>, so we use trim() here
690  if ( trim( $data ) != '' ) {
691  // Doing this here instead of in the skins to
692  // ensure that the div has the same ID in all
693  // skins
694  $data = "<div id='mw-data-after-content'>\n" .
695  "\t$data\n" .
696  "</div>\n";
697  }
698  } else {
699  wfDebug( "Hook SkinAfterContent changed output processing." );
700  }
701 
702  return $data;
703  }
704 
710  public function bottomScripts() {
711  // TODO and the suckage continues. This function is really just a wrapper around
712  // OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
713  // up at some point
714  $chunks = [ $this->getOutput()->getBottomScripts() ];
715 
716  // Keep the hook appendage separate to preserve WrappedString objects.
717  // This enables BaseTemplate::getTrail() to merge them where possible.
718  $extraHtml = '';
719  $this->getHookRunner()->onSkinAfterBottomScripts( $this, $extraHtml );
720  if ( $extraHtml !== '' ) {
721  $chunks[] = $extraHtml;
722  }
723  return WrappedString::join( "\n", $chunks );
724  }
725 
733  public function printSource() {
734  $title = $this->getTitle();
735  $oldid = $this->getOutput()->getRevisionId();
736  if ( $oldid ) {
737  $canonicalUrl = $title->getCanonicalURL( 'oldid=' . $oldid );
738  $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
739  } else {
740  // oldid not available for non existing pages
741  $url = htmlspecialchars( wfExpandIRI( $title->getCanonicalURL() ) );
742  }
743 
744  return $this->msg( 'retrievedfrom' )
745  ->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
746  ->parse();
747  }
748 
752  public function getUndeleteLink() {
753  $action = $this->getRequest()->getVal( 'action', 'view' );
754  $title = $this->getTitle();
755  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
756 
757  if ( ( !$title->exists() || $action == 'history' ) &&
758  $this->getAuthority()->probablyCan( 'deletedhistory', $title )
759  ) {
760  $n = $title->getDeletedEditsCount();
761 
762  if ( $n ) {
763  if ( $this->getAuthority()->probablyCan( 'undelete', $title ) ) {
764  $msg = 'thisisdeleted';
765  } else {
766  $msg = 'viewdeleted';
767  }
768 
769  $subtitle = $this->msg( $msg )->rawParams(
770  $linkRenderer->makeKnownLink(
771  SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ),
772  $this->msg( 'restorelink' )->numParams( $n )->text() )
773  )->escaped();
774 
775  $links = [];
776  // Add link to page logs, unless we're on the history page (which
777  // already has one)
778  if ( $action !== 'history' ) {
779  $links[] = $linkRenderer->makeKnownLink(
780  SpecialPage::getTitleFor( 'Log' ),
781  $this->msg( 'viewpagelogs-lowercase' )->text(),
782  [],
783  [ 'page' => $title->getPrefixedText() ]
784  );
785  }
786 
787  // Allow extensions to add more links
788  $this->getHookRunner()->onUndeletePageToolLinks(
789  $this->getContext(), $linkRenderer, $links );
790 
791  if ( $links ) {
792  $subtitle .= ''
793  . $this->msg( 'word-separator' )->escaped()
794  . $this->msg( 'parentheses' )
795  ->rawParams( $this->getLanguage()->pipeList( $links ) )
796  ->escaped();
797  }
798 
799  return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
800  }
801  }
802 
803  return '';
804  }
805 
811  public function subPageSubtitle( $out ) {
812  wfDeprecated( __METHOD__, '1.36' );
813  return $this->subPageSubtitleInternal();
814  }
815 
819  private function subPageSubtitleInternal() {
820  $services = MediaWikiServices::getInstance();
821  $linkRenderer = $services->getLinkRenderer();
822  $out = $this->getOutput();
823  $title = $out->getTitle();
824  $subpages = '';
825 
826  if ( !$this->getHookRunner()->onSkinSubPageSubtitle( $subpages, $this, $out ) ) {
827  return $subpages;
828  }
829 
830  $hasSubpages = $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() );
831  if ( !$out->isArticle() || !$hasSubpages ) {
832  return $subpages;
833  }
834 
835  $ptext = $title->getPrefixedText();
836  if ( strpos( $ptext, '/' ) !== false ) {
837  $links = explode( '/', $ptext );
838  array_pop( $links );
839  $count = 0;
840  $growingLink = '';
841  $display = '';
842  $lang = $this->getLanguage();
843 
844  foreach ( $links as $link ) {
845  $growingLink .= $link;
846  $display .= $link;
847  $linkObj = Title::newFromText( $growingLink );
848 
849  if ( $linkObj && $linkObj->isKnown() ) {
850  $getlink = $linkRenderer->makeKnownLink( $linkObj, $display );
851 
852  $count++;
853 
854  if ( $count > 1 ) {
855  $subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
856  } else {
857  $subpages .= '&lt; ';
858  }
859 
860  $subpages .= $getlink;
861  $display = '';
862  } else {
863  $display .= '/';
864  }
865  $growingLink .= '/';
866  }
867  }
868 
869  return $subpages;
870  }
871 
876  protected function getSearchLink() {
877  wfDeprecated( __METHOD__, '1.36' );
878 
879  $searchPage = $this->getSearchPageTitle();
880  return $searchPage->getLocalURL();
881  }
882 
887  public function getCopyright( $type = 'detect' ) {
888  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
889  if ( $type == 'detect' ) {
890  if ( !$this->getOutput()->isRevisionCurrent()
891  && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
892  ) {
893  $type = 'history';
894  } else {
895  $type = 'normal';
896  }
897  }
898 
899  if ( $type == 'history' ) {
900  $msg = 'history_copyright';
901  } else {
902  $msg = 'copyright';
903  }
904 
905  $config = $this->getConfig();
906 
907  if ( $config->get( 'RightsPage' ) ) {
908  $title = Title::newFromText( $config->get( 'RightsPage' ) );
909  $link = $linkRenderer->makeKnownLink(
910  $title, new HtmlArmor( $config->get( 'RightsText' ) ?: $title->getText() )
911  );
912  } elseif ( $config->get( 'RightsUrl' ) ) {
913  $link = Linker::makeExternalLink( $config->get( 'RightsUrl' ), $config->get( 'RightsText' ) );
914  } elseif ( $config->get( 'RightsText' ) ) {
915  $link = $config->get( 'RightsText' );
916  } else {
917  # Give up now
918  return '';
919  }
920 
921  // Allow for site and per-namespace customization of copyright notice.
922  $this->getHookRunner()->onSkinCopyrightFooter( $this->getTitle(), $type, $msg, $link );
923 
924  return $this->msg( $msg )->rawParams( $link )->text();
925  }
926 
930  protected function getCopyrightIcon() {
931  $out = '';
932  $config = $this->getConfig();
933 
934  $footerIcons = $config->get( 'FooterIcons' );
935  if (
936  isset( $footerIcons['copyright']['copyright'] ) &&
937  $footerIcons['copyright']['copyright']
938  ) {
939  $out = $footerIcons['copyright']['copyright'];
940  } elseif ( $config->get( 'RightsIcon' ) ) {
941  $icon = htmlspecialchars( $config->get( 'RightsIcon' ) );
942  $url = $config->get( 'RightsUrl' );
943 
944  if ( $url ) {
945  $out .= '<a href="' . htmlspecialchars( $url ) . '">';
946  }
947 
948  $text = htmlspecialchars( $config->get( 'RightsText' ) );
949  $out .= "<img src=\"$icon\" alt=\"$text\" width=\"88\" height=\"31\" />";
950 
951  if ( $url ) {
952  $out .= '</a>';
953  }
954  }
955 
956  return $out;
957  }
958 
963  protected function getPoweredBy() {
964  $resourceBasePath = $this->getConfig()->get( 'ResourceBasePath' );
965  $url1 = htmlspecialchars(
966  "$resourceBasePath/resources/assets/poweredby_mediawiki_88x31.png"
967  );
968  $url1_5 = htmlspecialchars(
969  "$resourceBasePath/resources/assets/poweredby_mediawiki_132x47.png"
970  );
971  $url2 = htmlspecialchars(
972  "$resourceBasePath/resources/assets/poweredby_mediawiki_176x62.png"
973  );
974  $text = '<a href="https://www.mediawiki.org/"><img src="' . $url1
975  . '" srcset="' . $url1_5 . ' 1.5x, ' . $url2 . ' 2x" '
976  . 'height="31" width="88" alt="Powered by MediaWiki" loading="lazy" /></a>';
977  $this->getHookRunner()->onSkinGetPoweredBy( $text, $this );
978  return $text;
979  }
980 
986  protected function lastModified() {
987  $timestamp = $this->getOutput()->getRevisionTimestamp();
988  $user = $this->getUser();
989  $language = $this->getLanguage();
990 
991  # No cached timestamp, load it from the database
992  if ( $timestamp === null ) {
993  $revId = $this->getOutput()->getRevisionId();
994  if ( $revId !== null ) {
995  $timestamp = MediaWikiServices::getInstance()
996  ->getRevisionLookup()
997  ->getTimestampFromId( $revId );
998  }
999  }
1000 
1001  if ( $timestamp ) {
1002  $d = $language->userDate( $timestamp, $user );
1003  $t = $language->userTime( $timestamp, $user );
1004  $s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->parse();
1005  } else {
1006  $s = '';
1007  }
1008 
1009  if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getLaggedReplicaMode() ) {
1010  $s .= ' <strong>' . $this->msg( 'laggedreplicamode' )->parse() . '</strong>';
1011  }
1012 
1013  return $s;
1014  }
1015 
1020  public function logoText( $align = '' ) {
1021  if ( $align != '' ) {
1022  $a = " style='float: {$align};'";
1023  } else {
1024  $a = '';
1025  }
1026 
1027  $mp = $this->msg( 'mainpage' )->escaped();
1028  $mptitle = Title::newMainPage();
1029  $url = ( is_object( $mptitle ) ? htmlspecialchars( $mptitle->getLocalURL() ) : '' );
1030 
1031  $logourl = ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() )[ '1x' ];
1032  return "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
1033  }
1034 
1043  public function makeFooterIcon( $icon, $withImage = 'withImage' ) {
1044  if ( is_string( $icon ) ) {
1045  $html = $icon;
1046  } else { // Assuming array
1047  $url = $icon['url'] ?? null;
1048  unset( $icon['url'] );
1049  if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
1050  // Lazy-load footer icons, since they're not part of the printed view.
1051  $icon['loading'] = 'lazy';
1052  // do this the lazy way, just pass icon data as an attribute array
1053  $html = Html::element( 'img', $icon );
1054  } else {
1055  $html = htmlspecialchars( $icon['alt'] ?? '' );
1056  }
1057  if ( $url ) {
1058  $html = Html::rawElement( 'a',
1059  [ 'href' => $url, 'target' => $this->getConfig()->get( 'ExternalLinkTarget' ) ],
1060  $html );
1061  }
1062  }
1063  return $html;
1064  }
1065 
1072  public function mainPageLink() {
1073  wfDeprecated( __METHOD__, '1.36' );
1074 
1075  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1076  $s = $linkRenderer->makeKnownLink(
1078  $this->msg( 'mainpage' )->text()
1079  );
1080 
1081  return $s;
1082  }
1083 
1101  public function footerLink( $desc, $page ) {
1102  $title = $this->footerLinkTitle( $desc, $page );
1103 
1104  if ( !$title ) {
1105  return '';
1106  }
1107 
1108  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1109  return $linkRenderer->makeKnownLink(
1110  $title,
1111  $this->msg( $desc )->text()
1112  );
1113  }
1114 
1120  private function footerLinkTitle( $desc, $page ) {
1121  // If the link description has been disabled in the default language,
1122  if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
1123  // then it is disabled, for all languages.
1124  return null;
1125  }
1126  // Otherwise, we display the link for the user, described in their
1127  // language (which may or may not be the same as the default language),
1128  // but we make the link target be the one site-wide page.
1129  $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
1130 
1131  return $title ?: null;
1132  }
1133 
1140  public function getSiteFooterLinks() {
1141  $callback = function () {
1142  return [
1143  'privacy' => $this->footerLink( 'privacy', 'privacypage' ),
1144  'about' => $this->footerLink( 'aboutsite', 'aboutpage' ),
1145  'disclaimer' => $this->footerLink( 'disclaimers', 'disclaimerpage' )
1146  ];
1147  };
1148 
1149  $services = MediaWikiServices::getInstance();
1150  $msgCache = $services->getMessageCache();
1151  $wanCache = $services->getMainWANObjectCache();
1152  $config = $this->getConfig();
1153 
1154  return ( $config->get( 'FooterLinkCacheExpiry' ) > 0 )
1155  ? $wanCache->getWithSetCallback(
1156  $wanCache->makeKey( 'footer-links' ),
1157  $config->get( 'FooterLinkCacheExpiry' ),
1158  $callback,
1159  [
1160  'checkKeys' => [
1161  // Unless there is both no exact $code override nor an i18n definition
1162  // in the software, the only MediaWiki page to check is for $code.
1163  $msgCache->getCheckKey( $this->getLanguage()->getCode() )
1164  ],
1165  'lockTSE' => 30
1166  ]
1167  )
1168  : $callback();
1169  }
1170 
1177  public function privacyLink() {
1178  wfDeprecated( __METHOD__, '1.36' );
1179  return $this->footerLink( 'privacy', 'privacypage' );
1180  }
1181 
1188  public function aboutLink() {
1189  wfDeprecated( __METHOD__, '1.36' );
1190  return $this->footerLink( 'aboutsite', 'aboutpage' );
1191  }
1192 
1199  public function disclaimerLink() {
1200  wfDeprecated( __METHOD__, '1.36' );
1201  return $this->footerLink( 'disclaimers', 'disclaimerpage' );
1202  }
1203 
1211  public function editUrlOptions() {
1212  $options = [ 'action' => 'edit' ];
1213  $out = $this->getOutput();
1214 
1215  if ( !$out->isRevisionCurrent() ) {
1216  $options['oldid'] = intval( $out->getRevisionId() );
1217  }
1218 
1219  return $options;
1220  }
1221 
1226  public function showEmailUser( $id ) {
1227  if ( $id instanceof UserIdentity ) {
1228  $targetUser = User::newFromIdentity( $id );
1229  } else {
1230  $targetUser = User::newFromId( $id );
1231  }
1232 
1233  # The sending user must have a confirmed email address and the receiving
1234  # user must accept emails from the sender.
1235  return $this->getUser()->canSendEmail()
1236  && SpecialEmailUser::validateTarget( $targetUser, $this->getUser() ) === '';
1237  }
1238 
1252  public function getSkinStylePath( $name ) {
1253  if ( $this->stylename === null ) {
1254  $class = static::class;
1255  throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
1256  }
1257 
1258  return $this->getConfig()->get( 'StylePath' ) . "/{$this->stylename}/$name";
1259  }
1260 
1261  /* these are used extensively in SkinTemplate, but also some other places */
1262 
1267  public static function makeMainPageUrl( $urlaction = '' ) {
1269  self::checkTitle( $title, '' );
1270 
1271  return $title->getLinkURL( $urlaction );
1272  }
1273 
1285  public static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1287  if ( $proto === null ) {
1288  return $title->getLocalURL( $urlaction );
1289  } else {
1290  return $title->getFullURL( $urlaction, false, $proto );
1291  }
1292  }
1293 
1300  public static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1301  $title = SpecialPage::getSafeTitleFor( $name, $subpage );
1302  return $title->getLocalURL( $urlaction );
1303  }
1304 
1311  public static function makeUrl( $name, $urlaction = '' ) {
1312  wfDeprecated( __METHOD__, '1.36' );
1313 
1314  $title = Title::newFromText( $name );
1315  self::checkTitle( $title, $name );
1316  return $title->getLocalURL( $urlaction );
1317  }
1318 
1325  public static function makeInternalOrExternalUrl( $name ) {
1326  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
1327  return $name;
1328  } else {
1329  $title = Title::newFromText( $name );
1330  self::checkTitle( $title, $name );
1331  return $title->getLocalURL();
1332  }
1333  }
1334 
1341  protected static function makeUrlDetails( $name, $urlaction = '' ) {
1342  $title = Title::newFromText( $name );
1343  self::checkTitle( $title, $name );
1344 
1345  return [
1346  'href' => $title->getLocalURL( $urlaction ),
1347  'exists' => $title->isKnown(),
1348  ];
1349  }
1350 
1357  protected static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1358  $title = Title::newFromText( $name );
1359  self::checkTitle( $title, $name );
1360 
1361  return [
1362  'href' => $title->getLocalURL( $urlaction ),
1363  'exists' => true
1364  ];
1365  }
1366 
1373  public static function checkTitle( &$title, $name ) {
1374  if ( !is_object( $title ) ) {
1375  $title = Title::newFromText( $name );
1376  if ( !is_object( $title ) ) {
1377  $title = Title::newFromText( '--error: link target missing--' );
1378  }
1379  }
1380  }
1381 
1390  public function mapInterwikiToLanguage( $code ) {
1391  $map = $this->getConfig()->get( 'InterlanguageLinkCodeMap' );
1392  return $map[ $code ] ?? $code;
1393  }
1394 
1403  public function getLanguages() {
1404  if ( $this->getConfig()->get( 'HideInterlanguageLinks' ) ) {
1405  return [];
1406  }
1407  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1408 
1409  $userLang = $this->getLanguage();
1410  $languageLinks = [];
1411  $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1412 
1413  foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
1414  $class = 'interlanguage-link interwiki-' . explode( ':', $languageLinkText, 2 )[0];
1415 
1416  $languageLinkTitle = Title::newFromText( $languageLinkText );
1417  if ( !$languageLinkTitle ) {
1418  continue;
1419  }
1420 
1421  $ilInterwikiCode = $this->mapInterwikiToLanguage( $languageLinkTitle->getInterwiki() );
1422 
1423  $ilLangName = $langNameUtils->getLanguageName( $ilInterwikiCode );
1424 
1425  if ( strval( $ilLangName ) === '' ) {
1426  $ilDisplayTextMsg = $this->msg( "interlanguage-link-$ilInterwikiCode" );
1427  if ( !$ilDisplayTextMsg->isDisabled() ) {
1428  // Use custom MW message for the display text
1429  $ilLangName = $ilDisplayTextMsg->text();
1430  } else {
1431  // Last resort: fallback to the language link target
1432  $ilLangName = $languageLinkText;
1433  }
1434  } else {
1435  // Use the language autonym as display text
1436  $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
1437  }
1438 
1439  // CLDR extension or similar is required to localize the language name;
1440  // otherwise we'll end up with the autonym again.
1441  $ilLangLocalName = $langNameUtils->getLanguageName(
1442  $ilInterwikiCode,
1443  $userLang->getCode()
1444  );
1445 
1446  $languageLinkTitleText = $languageLinkTitle->getText();
1447  if ( $ilLangLocalName === '' ) {
1448  $ilFriendlySiteName = $this->msg( "interlanguage-link-sitename-$ilInterwikiCode" );
1449  if ( !$ilFriendlySiteName->isDisabled() ) {
1450  if ( $languageLinkTitleText === '' ) {
1451  $ilTitle = $this->msg(
1452  'interlanguage-link-title-nonlangonly',
1453  $ilFriendlySiteName->text()
1454  )->text();
1455  } else {
1456  $ilTitle = $this->msg(
1457  'interlanguage-link-title-nonlang',
1458  $languageLinkTitleText,
1459  $ilFriendlySiteName->text()
1460  )->text();
1461  }
1462  } else {
1463  // we have nothing friendly to put in the title, so fall back to
1464  // displaying the interlanguage link itself in the title text
1465  // (similar to what is done in page content)
1466  $ilTitle = $languageLinkTitle->getInterwiki() .
1467  ":$languageLinkTitleText";
1468  }
1469  } elseif ( $languageLinkTitleText === '' ) {
1470  $ilTitle = $this->msg(
1471  'interlanguage-link-title-langonly',
1472  $ilLangLocalName
1473  )->text();
1474  } else {
1475  $ilTitle = $this->msg(
1476  'interlanguage-link-title',
1477  $languageLinkTitleText,
1478  $ilLangLocalName
1479  )->text();
1480  }
1481 
1482  $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
1483  $languageLink = [
1484  'href' => $languageLinkTitle->getFullURL(),
1485  'text' => $ilLangName,
1486  'title' => $ilTitle,
1487  'class' => $class,
1488  'link-class' => 'interlanguage-link-target',
1489  'lang' => $ilInterwikiCodeBCP47,
1490  'hreflang' => $ilInterwikiCodeBCP47,
1491  ];
1492  $hookContainer->run(
1493  'SkinTemplateGetLanguageLink',
1494  [ &$languageLink, $languageLinkTitle, $this->getTitle(), $this->getOutput() ],
1495  []
1496  );
1497  $languageLinks[] = $languageLink;
1498  }
1499 
1500  return $languageLinks;
1501  }
1502 
1509  protected function buildNavUrls() {
1510  $out = $this->getOutput();
1511  $title = $this->getTitle();
1512  $thispage = $title->getPrefixedDBkey();
1513  $uploadNavigationUrl = $this->getConfig()->get( 'UploadNavigationUrl' );
1514 
1515  $nav_urls = [];
1516  $nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1517  if ( $uploadNavigationUrl ) {
1518  $nav_urls['upload'] = [ 'href' => $uploadNavigationUrl ];
1519  } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
1520  $nav_urls['upload'] = [ 'href' => self::makeSpecialUrl( 'Upload' ) ];
1521  } else {
1522  $nav_urls['upload'] = false;
1523  }
1524  $nav_urls['specialpages'] = [ 'href' => self::makeSpecialUrl( 'Specialpages' ) ];
1525 
1526  $nav_urls['print'] = false;
1527  $nav_urls['permalink'] = false;
1528  $nav_urls['info'] = false;
1529  $nav_urls['whatlinkshere'] = false;
1530  $nav_urls['recentchangeslinked'] = false;
1531  $nav_urls['contributions'] = false;
1532  $nav_urls['log'] = false;
1533  $nav_urls['blockip'] = false;
1534  $nav_urls['mute'] = false;
1535  $nav_urls['emailuser'] = false;
1536  $nav_urls['userrights'] = false;
1537 
1538  // A print stylesheet is attached to all pages, but nobody ever
1539  // figures that out. :) Add a link...
1540  if ( !$out->isPrintable() && ( $out->isArticle() || $title->isSpecialPage() ) ) {
1541  $nav_urls['print'] = [
1542  'text' => $this->msg( 'printableversion' )->text(),
1543  'href' => 'javascript:print();'
1544  ];
1545  }
1546 
1547  if ( $out->isArticle() ) {
1548  // Also add a "permalink" while we're at it
1549  $revid = $out->getRevisionId();
1550  if ( $revid ) {
1551  $nav_urls['permalink'] = [
1552  'text' => $this->msg( 'permalink' )->text(),
1553  'href' => $title->getLocalURL( "oldid=$revid" )
1554  ];
1555  }
1556  }
1557 
1558  if ( $out->isArticleRelated() ) {
1559  $nav_urls['whatlinkshere'] = [
1560  'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $thispage )->getLocalURL()
1561  ];
1562 
1563  $nav_urls['info'] = [
1564  'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1565  'href' => $title->getLocalURL( "action=info" )
1566  ];
1567 
1568  if ( $title->exists() || $title->inNamespace( NS_CATEGORY ) ) {
1569  $nav_urls['recentchangeslinked'] = [
1570  'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $thispage )->getLocalURL()
1571  ];
1572  }
1573  }
1574 
1575  $user = $this->getRelevantUser();
1576 
1577  if ( $user ) {
1578  $rootUser = $user->getName();
1579 
1580  $nav_urls['contributions'] = [
1581  'text' => $this->msg( 'tool-link-contributions', $rootUser )->text(),
1582  'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1583  'tooltip-params' => [ $rootUser ],
1584  ];
1585 
1586  $nav_urls['log'] = [
1587  'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
1588  ];
1589 
1590  if ( $this->getAuthority()->isAllowed( 'block' ) ) {
1591  $nav_urls['blockip'] = [
1592  'text' => $this->msg( 'blockip', $rootUser )->text(),
1593  'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser )
1594  ];
1595  }
1596 
1597  if ( $this->showEmailUser( $user ) ) {
1598  $nav_urls['emailuser'] = [
1599  'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1600  'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1601  'tooltip-params' => [ $rootUser ],
1602  ];
1603  }
1604 
1605  if ( $user->isRegistered() ) {
1606  if ( $this->getUser()->isRegistered() && $this->getConfig()->get( 'EnableSpecialMute' ) ) {
1607  $nav_urls['mute'] = [
1608  'text' => $this->msg( 'mute-preferences' )->text(),
1609  'href' => self::makeSpecialUrlSubpage( 'Mute', $rootUser )
1610  ];
1611  }
1612 
1613  $sur = new UserrightsPage;
1614  $sur->setContext( $this->getContext() );
1615  $canChange = $sur->userCanChangeRights( $user );
1616  $nav_urls['userrights'] = [
1617  'text' => $this->msg(
1618  $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly',
1619  $rootUser
1620  )->text(),
1621  'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
1622  ];
1623  }
1624  }
1625 
1626  return $nav_urls;
1627  }
1628 
1634  final protected function buildFeedUrls() {
1635  $feeds = [];
1636  $out = $this->getOutput();
1637  if ( $out->isSyndicated() ) {
1638  foreach ( $out->getSyndicationLinks() as $format => $link ) {
1639  $feeds[$format] = [
1640  // Messages: feed-atom, feed-rss
1641  'text' => $this->msg( "feed-$format" )->text(),
1642  'href' => $link
1643  ];
1644  }
1645  }
1646  return $feeds;
1647  }
1648 
1673  public function buildSidebar() {
1674  $services = MediaWikiServices::getInstance();
1675  $callback = function ( $old = null, &$ttl = null ) {
1676  $bar = [];
1677  $this->addToSidebar( $bar, 'sidebar' );
1678  $this->getHookRunner()->onSkinBuildSidebar( $this, $bar );
1679  $msgCache = MediaWikiServices::getInstance()->getMessageCache();
1680  if ( $msgCache->isDisabled() ) {
1681  $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
1682  }
1683 
1684  return $bar;
1685  };
1686 
1687  $msgCache = $services->getMessageCache();
1688  $wanCache = $services->getMainWANObjectCache();
1689  $config = $this->getConfig();
1690  $languageCode = $this->getLanguage()->getCode();
1691 
1692  $sidebar = $config->get( 'EnableSidebarCache' )
1693  ? $wanCache->getWithSetCallback(
1694  $wanCache->makeKey( 'sidebar', $languageCode ),
1695  $config->get( 'SidebarCacheExpiry' ),
1696  $callback,
1697  [
1698  'checkKeys' => [
1699  // Unless there is both no exact $code override nor an i18n definition
1700  // in the software, the only MediaWiki page to check is for $code.
1701  $msgCache->getCheckKey( $languageCode )
1702  ],
1703  'lockTSE' => 30
1704  ]
1705  )
1706  : $callback();
1707 
1708  $sidebar['TOOLBOX'] = $this->makeToolbox(
1709  $this->buildNavUrls(),
1710  $this->buildFeedUrls()
1711  );
1712  $sidebar['LANGUAGES'] = $this->getLanguages();
1713  // Apply post-processing to the cached value
1714  $this->getHookRunner()->onSidebarBeforeOutput( $this, $sidebar );
1715 
1716  return $sidebar;
1717  }
1718 
1728  public function addToSidebar( &$bar, $message ) {
1729  $this->addToSidebarPlain( $bar, $this->msg( $message )->inContentLanguage()->plain() );
1730  }
1731 
1739  public function addToSidebarPlain( &$bar, $text ) {
1740  $lines = explode( "\n", $text );
1741 
1742  $heading = '';
1743  $config = $this->getConfig();
1744  $messageTitle = $config->get( 'EnableSidebarCache' )
1745  ? Title::newMainPage() : $this->getTitle();
1746  $messageCache = MediaWikiServices::getInstance()->getMessageCache();
1747 
1748  foreach ( $lines as $line ) {
1749  if ( strpos( $line, '*' ) !== 0 ) {
1750  continue;
1751  }
1752  $line = rtrim( $line, "\r" ); // for Windows compat
1753 
1754  if ( strpos( $line, '**' ) !== 0 ) {
1755  $heading = trim( $line, '* ' );
1756  if ( !array_key_exists( $heading, $bar ) ) {
1757  $bar[$heading] = [];
1758  }
1759  } else {
1760  $line = trim( $line, '* ' );
1761 
1762  if ( strpos( $line, '|' ) !== false ) { // sanity check
1763  $line = $messageCache->transform( $line, false, null, $messageTitle );
1764  $line = array_map( 'trim', explode( '|', $line, 2 ) );
1765  if ( count( $line ) !== 2 ) {
1766  // Second sanity check, could be hit by people doing
1767  // funky stuff with parserfuncs... (T35321)
1768  continue;
1769  }
1770 
1771  $extraAttribs = [];
1772 
1773  $msgLink = $this->msg( $line[0] )->page( $messageTitle )->inContentLanguage();
1774  if ( $msgLink->exists() ) {
1775  $link = $msgLink->text();
1776  if ( $link == '-' ) {
1777  continue;
1778  }
1779  } else {
1780  $link = $line[0];
1781  }
1782  $msgText = $this->msg( $line[1] )->page( $messageTitle );
1783  if ( $msgText->exists() ) {
1784  $text = $msgText->text();
1785  } else {
1786  $text = $line[1];
1787  }
1788 
1789  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
1790  $href = $link;
1791 
1792  // Parser::getExternalLinkAttribs won't work here because of the Namespace things
1793  if ( $config->get( 'NoFollowLinks' ) &&
1794  !wfMatchesDomainList( $href, $config->get( 'NoFollowDomainExceptions' ) )
1795  ) {
1796  $extraAttribs['rel'] = 'nofollow';
1797  }
1798 
1799  if ( $config->get( 'ExternalLinkTarget' ) ) {
1800  $extraAttribs['target'] = $config->get( 'ExternalLinkTarget' );
1801  }
1802  } else {
1803  $title = Title::newFromText( $link );
1804 
1805  if ( $title ) {
1806  $title = $title->fixSpecialName();
1807  $href = $title->getLinkURL();
1808  } else {
1809  $href = 'INVALID-TITLE';
1810  }
1811  }
1812 
1813  $bar[$heading][] = array_merge( [
1814  'text' => $text,
1815  'href' => $href,
1816  'id' => Sanitizer::escapeIdForAttribute( 'n-' . strtr( $line[1], ' ', '-' ) ),
1817  'active' => false,
1818  ], $extraAttribs );
1819  } else {
1820  continue;
1821  }
1822  }
1823  }
1824 
1825  return $bar;
1826  }
1827 
1833  public function getNewtalks() {
1834  $newMessagesAlert = '';
1835  $user = $this->getUser();
1836  $services = MediaWikiServices::getInstance();
1837  $linkRenderer = $services->getLinkRenderer();
1838  $userHasNewMessages = $services->getTalkPageNotificationManager()
1839  ->userHasNewMessages( $user );
1840  $timestamp = $services->getTalkPageNotificationManager()
1841  ->getLatestSeenMessageTimestamp( $user );
1842  $newtalks = !$userHasNewMessages ? [] : [
1843  [
1844  // TODO: Deprecate adding wiki and link to array and redesign GetNewMessagesAlert hook
1845  'wiki' => WikiMap::getCurrentWikiId(),
1846  'link' => $user->getTalkPage()->getLocalURL(),
1847  'rev' => $timestamp ? $services->getRevisionLookup()
1848  ->getRevisionByTimestamp( $user->getTalkPage(), $timestamp ) : null
1849  ]
1850  ];
1851  $out = $this->getOutput();
1852 
1853  // Allow extensions to disable or modify the new messages alert
1854  if ( !$this->getHookRunner()->onGetNewMessagesAlert(
1855  $newMessagesAlert, $newtalks, $user, $out )
1856  ) {
1857  return '';
1858  }
1859  if ( $newMessagesAlert ) {
1860  return $newMessagesAlert;
1861  }
1862 
1863  if ( $newtalks !== [] ) {
1864  $uTalkTitle = $user->getTalkPage();
1865  $lastSeenRev = $newtalks[0]['rev'];
1866  $numAuthors = 0;
1867  if ( $lastSeenRev !== null ) {
1868  $plural = true; // Default if we have a last seen revision: if unknown, use plural
1869  $revStore = $services->getRevisionStore();
1870  $latestRev = $revStore->getRevisionByTitle(
1871  $uTalkTitle,
1872  0,
1873  RevisionLookup::READ_NORMAL
1874  );
1875  if ( $latestRev !== null ) {
1876  // Singular if only 1 unseen revision, plural if several unseen revisions.
1877  $plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1878  $numAuthors = $revStore->countAuthorsBetween(
1879  $uTalkTitle->getArticleID(),
1880  $lastSeenRev,
1881  $latestRev,
1882  null,
1883  10,
1884  RevisionStore::INCLUDE_NEW
1885  );
1886  }
1887  } else {
1888  // Singular if no revision -> diff link will show latest change only in any case
1889  $plural = false;
1890  }
1891  $plural = $plural ? 999 : 1;
1892  // 999 signifies "more than one revision". We don't know how many, and even if we did,
1893  // the number of revisions or authors is not necessarily the same as the number of
1894  // "messages".
1895  $newMessagesLink = $linkRenderer->makeKnownLink(
1896  $uTalkTitle,
1897  $this->msg( 'newmessageslinkplural' )->params( $plural )->text(),
1898  [],
1899  $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
1900  );
1901 
1902  $newMessagesDiffLink = $linkRenderer->makeKnownLink(
1903  $uTalkTitle,
1904  $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->text(),
1905  [],
1906  $lastSeenRev !== null
1907  ? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1908  : [ 'diff' => 'cur' ]
1909  );
1910 
1911  if ( $numAuthors >= 1 && $numAuthors <= 10 ) {
1912  $newMessagesAlert = $this->msg(
1913  'youhavenewmessagesfromusers',
1914  $newMessagesLink,
1915  $newMessagesDiffLink
1916  )->numParams( $numAuthors, $plural );
1917  } else {
1918  // $numAuthors === 11 signifies "11 or more" ("more than 10")
1919  $newMessagesAlert = $this->msg(
1920  $numAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
1921  $newMessagesLink,
1922  $newMessagesDiffLink
1923  )->numParams( $plural );
1924  }
1925  $newMessagesAlert = $newMessagesAlert->text();
1926  // Disable CDN cache
1927  $out->setCdnMaxage( 0 );
1928  }
1929 
1930  return $newMessagesAlert;
1931  }
1932 
1940  private function getCachedNotice( $name ) {
1941  $config = $this->getConfig();
1942 
1943  if ( $name === 'default' ) {
1944  // special case
1945  $notice = $config->get( 'SiteNotice' );
1946  if ( empty( $notice ) ) {
1947  return false;
1948  }
1949  } else {
1950  $msg = $this->msg( $name )->inContentLanguage();
1951  if ( $msg->isBlank() ) {
1952  return '';
1953  } elseif ( $msg->isDisabled() ) {
1954  return false;
1955  }
1956  $notice = $msg->plain();
1957  }
1958 
1959  $services = MediaWikiServices::getInstance();
1960  $cache = $services->getMainWANObjectCache();
1961  $parsed = $cache->getWithSetCallback(
1962  // Use the extra hash appender to let eg SSL variants separately cache
1963  // Key is verified with md5 hash of unparsed wikitext
1964  $cache->makeKey( $name, $config->get( 'RenderHashAppend' ), md5( $notice ) ),
1965  // TTL in seconds
1966  600,
1967  function () use ( $notice ) {
1968  return $this->getOutput()->parseAsInterface( $notice );
1969  }
1970  );
1971 
1972  $contLang = $services->getContentLanguage();
1973  return Html::rawElement(
1974  'div',
1975  [
1976  'id' => 'localNotice',
1977  'lang' => $contLang->getHtmlCode(),
1978  'dir' => $contLang->getDir()
1979  ],
1980  $parsed
1981  );
1982  }
1983 
1987  public function getSiteNotice() {
1988  $siteNotice = '';
1989 
1990  if ( $this->getHookRunner()->onSiteNoticeBefore( $siteNotice, $this ) ) {
1991  if ( $this->getUser()->isRegistered() ) {
1992  $siteNotice = $this->getCachedNotice( 'sitenotice' );
1993  } else {
1994  $anonNotice = $this->getCachedNotice( 'anonnotice' );
1995  if ( $anonNotice === false ) {
1996  $siteNotice = $this->getCachedNotice( 'sitenotice' );
1997  } else {
1998  $siteNotice = $anonNotice;
1999  }
2000  }
2001  if ( $siteNotice === false ) {
2002  $siteNotice = $this->getCachedNotice( 'default' ) ?: '';
2003  }
2004  }
2005 
2006  $this->getHookRunner()->onSiteNoticeAfter( $siteNotice, $this );
2007  return $siteNotice;
2008  }
2009 
2022  public function doEditSectionLink( Title $nt, $section, $tooltip, Language $lang ) {
2023  // HTML generated here should probably have userlangattributes
2024  // added to it for LTR text on RTL pages
2025 
2026  $attribs = [];
2027  if ( $tooltip !== null ) {
2028  $attribs['title'] = $this->msg( 'editsectionhint' )->rawParams( $tooltip )
2029  ->inLanguage( $lang )->text();
2030  }
2031 
2032  $links = [
2033  'editsection' => [
2034  'text' => $this->msg( 'editsection' )->inLanguage( $lang )->text(),
2035  'targetTitle' => $nt,
2036  'attribs' => $attribs,
2037  'query' => [ 'action' => 'edit', 'section' => $section ]
2038  ]
2039  ];
2040 
2041  $this->getHookRunner()->onSkinEditSectionLinks( $this, $nt, $section, $tooltip, $links, $lang );
2042 
2043  $result = '<span class="mw-editsection"><span class="mw-editsection-bracket">[</span>';
2044 
2045  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
2046  $linksHtml = [];
2047  foreach ( $links as $k => $linkDetails ) {
2048  $linksHtml[] = $linkRenderer->makeKnownLink(
2049  $linkDetails['targetTitle'],
2050  $linkDetails['text'],
2051  $linkDetails['attribs'],
2052  $linkDetails['query']
2053  );
2054  }
2055 
2056  $result .= implode(
2057  '<span class="mw-editsection-divider">'
2058  . $this->msg( 'pipe-separator' )->inLanguage( $lang )->escaped()
2059  . '</span>',
2060  $linksHtml
2061  );
2062 
2063  $result .= '<span class="mw-editsection-bracket">]</span></span>';
2064  return $result;
2065  }
2066 
2076  public function makeToolbox( $navUrls, $feedUrls ) {
2077  $toolbox = [];
2078  if ( $navUrls['whatlinkshere'] ?? null ) {
2079  $toolbox['whatlinkshere'] = $navUrls['whatlinkshere'];
2080  $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
2081  }
2082  if ( $navUrls['recentchangeslinked'] ?? null ) {
2083  $toolbox['recentchangeslinked'] = $navUrls['recentchangeslinked'];
2084  $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
2085  $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
2086  $toolbox['recentchangeslinked']['rel'] = 'nofollow';
2087  }
2088  if ( $feedUrls ) {
2089  $toolbox['feeds']['id'] = 'feedlinks';
2090  $toolbox['feeds']['links'] = [];
2091  foreach ( $feedUrls as $key => $feed ) {
2092  $toolbox['feeds']['links'][$key] = $feed;
2093  $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
2094  $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
2095  $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
2096  $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
2097  }
2098  }
2099  foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
2100  'userrights', 'upload', 'specialpages' ] as $special
2101  ) {
2102  if ( $navUrls[$special] ?? null ) {
2103  $toolbox[$special] = $navUrls[$special];
2104  $toolbox[$special]['id'] = "t-$special";
2105  }
2106  }
2107  if ( $navUrls['print'] ?? null ) {
2108  $toolbox['print'] = $navUrls['print'];
2109  $toolbox['print']['id'] = 't-print';
2110  $toolbox['print']['rel'] = 'alternate';
2111  $toolbox['print']['msg'] = 'printableversion';
2112  }
2113  if ( $navUrls['permalink'] ?? null ) {
2114  $toolbox['permalink'] = $navUrls['permalink'];
2115  $toolbox['permalink']['id'] = 't-permalink';
2116  }
2117  if ( $navUrls['info'] ?? null ) {
2118  $toolbox['info'] = $navUrls['info'];
2119  $toolbox['info']['id'] = 't-info';
2120  }
2121 
2122  return $toolbox;
2123  }
2124 
2142  final public function getIndicatorsHTML( $indicators ) {
2143  wfDeprecated( __METHOD__, '1.36' );
2144 
2145  $out = "<div class=\"mw-indicators mw-body-content\">\n";
2146  foreach ( $this->getIndicatorsData( $indicators ) as $indicatorData ) {
2147  $out .= Html::rawElement(
2148  'div',
2149  [
2150  'id' => $indicatorData['id'],
2151  'class' => $indicatorData['class']
2152  ],
2153  $indicatorData['html']
2154  ) . "\n";
2155  }
2156  $out .= "</div>\n";
2157  return $out;
2158  }
2159 
2166  protected function getIndicatorsData( $indicators ) {
2167  $indicatorData = [];
2168  foreach ( $indicators as $id => $content ) {
2169  $indicatorData[] = [
2170  'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
2171  'class' => 'mw-indicator',
2172  'html' => $content,
2173  ];
2174  }
2175  return $indicatorData;
2176  }
2177 
2192  final public function getPersonalToolsForMakeListItem( $urls, $applyClassesToListItems = false ) {
2193  $personal_tools = [];
2194  foreach ( $urls as $key => $plink ) {
2195  # The class on a personal_urls item is meant to go on the <a> instead
2196  # of the <li> so we have to use a single item "links" array instead
2197  # of using most of the personal_url's keys directly.
2198  $ptool = [
2199  'links' => [
2200  [ 'single-id' => "pt-$key" ],
2201  ],
2202  'id' => "pt-$key",
2203  ];
2204  if ( $applyClassesToListItems && isset( $plink['class'] ) ) {
2205  $ptool['class'] = $plink['class'];
2206  }
2207  if ( isset( $plink['active'] ) ) {
2208  $ptool['active'] = $plink['active'];
2209  }
2210  // Set class for the link to link-class, when defined.
2211  // This allows newer notifications content navigation to retain their classes
2212  // when merged back into the personal tools.
2213  // Doing this here allows the loop below to overwrite the class if defined directly.
2214  if ( isset( $plink['link-class'] ) ) {
2215  $ptool['links'][0]['class'] = $plink['link-class'];
2216  }
2217  $props = [
2218  'href',
2219  'text',
2220  'dir',
2221  'data',
2222  'exists',
2223  'data-mw'
2224  ];
2225  if ( !$applyClassesToListItems ) {
2226  $props[] = 'class';
2227  }
2228  foreach ( $props as $k ) {
2229  if ( isset( $plink[$k] ) ) {
2230  $ptool['links'][0][$k] = $plink[$k];
2231  }
2232  }
2233  $personal_tools[$key] = $ptool;
2234  }
2235  return $personal_tools;
2236  }
2237 
2294  final public function makeLink( $key, $item, $linkOptions = [] ) {
2295  $options = $linkOptions + $this->defaultLinkOptions;
2296  $text = $item['text'] ?? $this->msg( $item['msg'] ?? $key )->text();
2297 
2298  $html = htmlspecialchars( $text );
2299 
2300  if ( isset( $options['text-wrapper'] ) ) {
2301  $wrapper = $options['text-wrapper'];
2302  if ( isset( $wrapper['tag'] ) ) {
2303  $wrapper = [ $wrapper ];
2304  }
2305  while ( count( $wrapper ) > 0 ) {
2306  $element = array_pop( $wrapper );
2307  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
2308  $html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
2309  }
2310  }
2311 
2312  if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
2313  $attrs = $item;
2314  foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
2315  'tooltip-params', 'exists' ] as $k ) {
2316  unset( $attrs[$k] );
2317  }
2318 
2319  if ( isset( $attrs['data'] ) ) {
2320  foreach ( $attrs['data'] as $key => $value ) {
2321  $attrs[ 'data-' . $key ] = $value;
2322  }
2323  unset( $attrs[ 'data' ] );
2324  }
2325 
2326  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2327  $item['single-id'] = $item['id'];
2328  }
2329 
2330  $tooltipParams = [];
2331  if ( isset( $item['tooltip-params'] ) ) {
2332  $tooltipParams = $item['tooltip-params'];
2333  }
2334 
2335  if ( isset( $item['single-id'] ) ) {
2336  $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
2337 
2338  if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
2339  $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
2340  if ( $title !== false ) {
2341  $attrs['title'] = $title;
2342  }
2343  } else {
2345  $item['single-id'],
2346  $tooltipParams,
2347  $tooltipOption
2348  );
2349  if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
2350  $attrs['title'] = $tip['title'];
2351  }
2352  if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
2353  $attrs['accesskey'] = $tip['accesskey'];
2354  }
2355  }
2356  }
2357  if ( isset( $options['link-class'] ) ) {
2358  if ( isset( $attrs['class'] ) ) {
2359  // In the future, this should accept an array of classes, not a string
2360  if ( is_array( $attrs['class'] ) ) {
2361  $attrs['class'][] = $options['link-class'];
2362  } else {
2363  $attrs['class'] .= " {$options['link-class']}";
2364  }
2365  } else {
2366  $attrs['class'] = $options['link-class'];
2367  }
2368  }
2369  $html = Html::rawElement( isset( $attrs['href'] )
2370  ? 'a'
2371  : $options['link-fallback'], $attrs, $html );
2372  }
2373 
2374  return $html;
2375  }
2376 
2411  final public function makeListItem( $key, $item, $options = [] ) {
2412  // In case this is still set from SkinTemplate, we don't want it to appear in
2413  // the HTML output (normally removed in SkinTemplate::buildContentActionUrls())
2414  unset( $item['redundant'] );
2415 
2416  if ( isset( $item['links'] ) ) {
2417  $links = [];
2418  foreach ( $item['links'] as $link ) {
2419  // Note: links will have identical label unless 'msg' is set on $link
2420  $links[] = $this->makeLink( $key, $link, $options );
2421  }
2422  $html = implode( ' ', $links );
2423  } else {
2424  $link = $item;
2425  // These keys are used by makeListItem and shouldn't be passed on to the link
2426  foreach ( [ 'id', 'class', 'active', 'tag', 'itemtitle' ] as $k ) {
2427  unset( $link[$k] );
2428  }
2429  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2430  // The id goes on the <li> not on the <a> for single links
2431  // but makeSidebarLink still needs to know what id to use when
2432  // generating tooltips and accesskeys.
2433  $link['single-id'] = $item['id'];
2434  }
2435  if ( isset( $link['link-class'] ) ) {
2436  // link-class should be set on the <a> itself,
2437  // so pass it in as 'class'
2438  $link['class'] = $link['link-class'];
2439  unset( $link['link-class'] );
2440  }
2441  $html = $this->makeLink( $key, $link, $options );
2442  }
2443 
2444  $attrs = [];
2445  foreach ( [ 'id', 'class' ] as $attr ) {
2446  if ( isset( $item[$attr] ) ) {
2447  $attrs[$attr] = $item[$attr];
2448  }
2449  }
2450  if ( isset( $item['active'] ) && $item['active'] ) {
2451  if ( !isset( $attrs['class'] ) ) {
2452  $attrs['class'] = '';
2453  }
2454 
2455  // In the future, this should accept an array of classes, not a string
2456  if ( is_array( $attrs['class'] ) ) {
2457  $attrs['class'][] = 'active';
2458  } else {
2459  $attrs['class'] .= ' active';
2460  $attrs['class'] = trim( $attrs['class'] );
2461  }
2462  }
2463  if ( isset( $item['itemtitle'] ) ) {
2464  $attrs['title'] = $item['itemtitle'];
2465  }
2466  return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
2467  }
2468 
2475  final public function makeSearchInput( $attrs = [] ) {
2476  $autoCapHint = $this->getConfig()->get( 'CapitalLinks' );
2477  $realAttrs = [
2478  'type' => 'search',
2479  'name' => 'search',
2480  'placeholder' => $this->msg( 'searchsuggest-search' )->text(),
2481  // T251664: Disable autocapitalization of input
2482  // method when using fully case-sensitive titles.
2483  'autocapitalize' => $autoCapHint ? 'sentences' : 'none',
2484  ];
2485 
2486  $realAttrs = array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
2487  return Html::element( 'input', $realAttrs );
2488  }
2489 
2497  final public function makeSearchButton( $mode, $attrs = [] ) {
2498  switch ( $mode ) {
2499  case 'go':
2500  case 'fulltext':
2501  $realAttrs = [
2502  'type' => 'submit',
2503  'name' => $mode,
2504  'value' => $this->msg( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
2505  ];
2506  $realAttrs = array_merge(
2507  $realAttrs,
2508  Linker::tooltipAndAccesskeyAttribs( "search-$mode" ),
2509  $attrs
2510  );
2511  return Html::element( 'input', $realAttrs );
2512  case 'image':
2513  $buttonAttrs = [
2514  'type' => 'submit',
2515  'name' => 'button',
2516  ];
2517  $buttonAttrs = array_merge(
2518  $buttonAttrs,
2519  Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ),
2520  $attrs
2521  );
2522  unset( $buttonAttrs['src'] );
2523  unset( $buttonAttrs['alt'] );
2524  unset( $buttonAttrs['width'] );
2525  unset( $buttonAttrs['height'] );
2526  $imgAttrs = [
2527  'src' => $attrs['src'],
2528  'alt' => $attrs['alt'] ?? $this->msg( 'searchbutton' )->text(),
2529  'width' => $attrs['width'] ?? null,
2530  'height' => $attrs['height'] ?? null,
2531  ];
2532  return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
2533  default:
2534  throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' );
2535  }
2536  }
2537 
2548  public function getAfterPortlet( string $name ): string {
2549  $html = '';
2550 
2551  $this->getHookRunner()->onSkinAfterPortlet( $this, $name, $html );
2552 
2553  return $html;
2554  }
2555 
2561  final public function prepareSubtitle() {
2562  $out = $this->getOutput();
2563  $subpagestr = $this->subPageSubtitleInternal();
2564  if ( $subpagestr !== '' ) {
2565  $subpagestr = '<span class="subpages">' . $subpagestr . '</span>';
2566  }
2567  return $subpagestr . $out->getSubtitle();
2568  }
2569 
2581  protected function getFooterLinks(): array {
2582  $out = $this->getOutput();
2583  $title = $out->getTitle();
2584  $titleExists = $title->exists();
2585  $config = $this->getConfig();
2586  $maxCredits = $config->get( 'MaxCredits' );
2587  $showCreditsIfMax = $config->get( 'ShowCreditsIfMax' );
2588  $useCredits = $titleExists
2589  && $out->isArticle()
2590  && $out->isRevisionCurrent()
2591  && $maxCredits !== 0;
2592 
2594  if ( $useCredits ) {
2595  $article = Article::newFromWikiPage( $this->getWikiPage(), $this );
2596  $action = Action::factory( 'credits', $article, $this );
2597  }
2598 
2599  '@phan-var CreditsAction $action';
2600  $data = [
2601  'info' => [
2602  'lastmod' => !$useCredits ? $this->lastModified() : null,
2603  'numberofwatchingusers' => null,
2604  'credits' => $useCredits ?
2605  $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
2606  'copyright' => $titleExists &&
2607  $out->showsCopyright() ? $this->getCopyright() : null,
2608  ],
2609  'places' => $this->getSiteFooterLinks(),
2610  ];
2611  foreach ( $data as $key => $existingItems ) {
2612  $newItems = [];
2613  $this->getHookRunner()->onSkinAddFooterLinks( $this, $key, $newItems );
2614  $data[$key] = $existingItems + $newItems;
2615  }
2616  return $data;
2617  }
2618 
2619  public function getSearchPageTitle(): Title {
2620  return $this->searchPageTitle ?? SpecialPage::getTitleFor( 'Search' );
2621  }
2622 
2623  public function setSearchPageTitle( Title $title ) {
2624  $this->searchPageTitle = $title;
2625  }
2626 }
Skin\prepareSubtitle
prepareSubtitle()
Prepare the subtitle of the page for output in the skin if one has been set.
Definition: Skin.php:2561
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:35
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:408
Skin\$skinname
string null $skinname
Definition: Skin.php:55
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
Skin\editUrlOptions
editUrlOptions()
Return URL options for the 'edit page' link.
Definition: Skin.php:1211
Skin\showEmailUser
showEmailUser( $id)
Definition: Skin.php:1226
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:643
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:415
Skin\makeUrlDetails
static makeUrlDetails( $name, $urlaction='')
these return an array with the 'href' and boolean 'exists'
Definition: Skin.php:1341
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
Skin\getSiteNotice
getSiteNotice()
Definition: Skin.php:1987
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
Skin\makeLink
makeLink( $key, $item, $linkOptions=[])
Makes a link, usually used by makeListItem to generate a link for an item in a list used in navigatio...
Definition: Skin.php:2294
ResourceLoader\makeConfigSetScript
static makeConfigSetScript(array $configuration)
Return JS code which will set the MediaWiki configuration array to the given value.
Definition: ResourceLoader.php:1669
Skin\VERSION_MAJOR
const VERSION_MAJOR
The current major version of the skin specification.
Definition: Skin.php:75
Skin\buildFeedUrls
buildFeedUrls()
Build data structure representing syndication links.
Definition: Skin.php:1634
Skin\subPageSubtitleInternal
subPageSubtitleInternal()
Definition: Skin.php:819
Skin\$options
array $options
Skin options passed into constructor.
Definition: Skin.php:60
Action\factory
static factory(string $action, Article $article, IContextSource $context=null)
Get an appropriate Action subclass for the given action.
Definition: Action.php:85
Skin\footerLinkTitle
footerLinkTitle( $desc, $page)
Definition: Skin.php:1120
Skin\getPoweredBy
getPoweredBy()
Gets the powered by MediaWiki icon.
Definition: Skin.php:963
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
Skin\getPersonalToolsForMakeListItem
getPersonalToolsForMakeListItem( $urls, $applyClassesToListItems=false)
Create an array of personal tools items from the data in the quicktemplate stored by SkinTemplate.
Definition: Skin.php:2192
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
ResourceLoader\makeInlineScript
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
Definition: ResourceLoader.php:1642
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:811
true
return true
Definition: router.php:90
Skin\makeVariablesScript
static makeVariablesScript( $data, $nonce=null)
Definition: Skin.php:475
$fallback
$fallback
Definition: MessagesAb.php:11
Skin\lastModified
lastModified()
Get the timestamp of the latest revision, formatted in user language.
Definition: Skin.php:986
Skin\getSiteFooterLinks
getSiteFooterLinks()
Gets the link to the wiki's privacy policy, about page, and disclaimer page.
Definition: Skin.php:1140
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:1373
Skin\mapInterwikiToLanguage
mapInterwikiToLanguage( $code)
Allows correcting the language of interlanguage links which, mostly due to legacy reasons,...
Definition: Skin.php:1390
Skin\makeSpecialUrl
static makeSpecialUrl( $name, $urlaction='', $proto=null)
Make a URL for a Special Page using the given query and protocol.
Definition: Skin.php:1285
Skin\getCategoryLinks
getCategoryLinks()
Definition: Skin.php:550
Skin\aboutLink
aboutLink()
Gets the link to the wiki's about page.
Definition: Skin.php:1188
Skin\makeSpecialUrlSubpage
static makeSpecialUrlSubpage( $name, $subpage, $urlaction='')
Definition: Skin.php:1300
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:972
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:679
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:81
Title\newMainPage
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:742
ContextSource\getUser
getUser()
Definition: ContextSource.php:136
ContextSource\getTitle
getTitle()
Definition: ContextSource.php:90
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:844
WikiMap\getCurrentWikiId
static getCurrentWikiId()
Definition: WikiMap.php:303
Skin\getHtmlElementAttributes
getHtmlElementAttributes()
Return values for <html> element.
Definition: Skin.php:527
SpecialEmailUser\validateTarget
static validateTarget( $target, User $sender)
Validate target User.
Definition: SpecialEmailUser.php:219
Skin\doEditSectionLink
doEditSectionLink(Title $nt, $section, $tooltip, Language $lang)
Create a section edit link.
Definition: Skin.php:2022
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
Skin\makeListItem
makeListItem( $key, $item, $options=[])
Generates a list item for a navigation, portlet, portal, sidebar...
Definition: Skin.php:2411
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Skin\bottomScripts
bottomScripts()
This gets called shortly before the "</body>" tag.
Definition: Skin.php:710
Skin\mainPageLink
mainPageLink()
Gets the link to the wiki's main page.
Definition: Skin.php:1072
Linker\tooltipAndAccesskeyAttribs
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2457
Skin\getSkinNames
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:96
Skin\afterContentHook
afterContentHook()
This runs a hook to allow extensions placing their stuff after content and article metadata (e....
Definition: Skin.php:684
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:153
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:811
Skin\getCachedNotice
getCachedNotice( $name)
Get a cached notice.
Definition: Skin.php:1940
UserrightsPage
Special page to allow managing user group membership.
Definition: SpecialUserrights.php:37
Article\newFromWikiPage
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:196
MWException
MediaWiki exception.
Definition: MWException.php:29
Skin\getIndicatorsData
getIndicatorsData( $indicators)
Return an array of indicator data.
Definition: Skin.php:2166
Skin\buildSidebar
buildSidebar()
Build an array that represents the sidebar(s), the navigation bar among them.
Definition: Skin.php:1673
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:996
Skin\privacyLink
privacyLink()
Gets the link to the wiki's privacy policy page.
Definition: Skin.php:1177
Skin\normalizeKey
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:128
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:1101
Skin\setRelevantUser
setRelevantUser(?UserIdentity $u)
Definition: Skin.php:417
$wgFallbackSkin
$wgFallbackSkin
Fallback skin used when the skin defined by $wgDefaultSkin can't be found.
Definition: DefaultSettings.php:3837
Skin\$stylename
string $stylename
Stylesheets set to use.
Definition: Skin.php:72
Skin\getCategories
getCategories()
Definition: Skin.php:649
Skin\getSearchPageTitle
getSearchPageTitle()
Definition: Skin.php:2619
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:126
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:33
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:117
$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:1728
Skin\drawCategoryBrowser
drawCategoryBrowser( $tree)
Render the array as a series of links.
Definition: Skin.php:625
Skin\getLanguages
getLanguages()
Generates array of language links for the current page.
Definition: Skin.php:1403
$title
$title
Definition: testCompression.php:38
Skin\preloadExistence
preloadExistence()
Preload the existence of three commonly-requested pages in a single query.
Definition: Skin.php:348
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:1015
$wgDefaultSkin
$wgDefaultSkin
Default skin, for new users and anonymous visitors.
Definition: DefaultSettings.php:3830
Skin\disclaimerLink
disclaimerLink()
Gets the link to the wiki's general disclaimers page.
Definition: Skin.php:1199
Skin\getLogo
getLogo()
URL to the default square logo (1x key)
Definition: Skin.php:542
$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:894
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:50
Skin\makeToolbox
makeToolbox( $navUrls, $feedUrls)
Create an array of common toolbox items from the data in the quicktemplate stored by SkinTemplate.
Definition: Skin.php:2076
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:529
Skin\$mRelevantTitle
$mRelevantTitle
Definition: Skin.php:61
Skin\$searchPageTitle
$searchPageTitle
Definition: Skin.php:77
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
Skin\$defaultLinkOptions
array $defaultLinkOptions
link options used in Skin::makeLink.
Definition: Skin.php:50
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:702
Skin\getRelevantTitle
getRelevantTitle()
Return the "relevant" title.
Definition: Skin.php:409
$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:1252
Skin\getPageClasses
getPageClasses( $title)
TODO: document.
Definition: Skin.php:492
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
Skin\makeSearchInput
makeSearchInput( $attrs=[])
Definition: Skin.php:2475
Skin\getDefaultModules
getDefaultModules()
Defines the ResourceLoader modules that should be added to the skin It is recommended that skins wish...
Definition: Skin.php:266
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
Skin\getIndicatorsHTML
getIndicatorsHTML( $indicators)
Get the suggested HTML for page status indicators: icons (or short text snippets) usually displayed i...
Definition: Skin.php:2142
Skin\getSearchLink
getSearchLink()
Definition: Skin.php:876
$line
$line
Definition: mcc.php:119
Skin\printSource
printSource()
Text with the permalink to the source page, usually shown on the footer of a printed page.
Definition: Skin.php:733
OutputPage\getHTMLTitle
getHTMLTitle()
Return the "HTML title", i.e.
Definition: OutputPage.php:955
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:1509
Skin\getAfterPortlet
getAfterPortlet(string $name)
Allows extensions to hook into known portlets and add stuff to them.
Definition: Skin.php:2548
Skin\__construct
__construct( $options=null)
Definition: Skin.php:183
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:2278
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:45
Skin\logoText
logoText( $align='')
Definition: Skin.php:1020
$userOptionsLookup
UserOptionsLookup $userOptionsLookup
Definition: ApiWatchlistTrait.php:33
Skin\getNewtalks
getNewtalks()
Gets new talk page messages for the current user and returns an appropriate alert message (or an empt...
Definition: Skin.php:1833
Skin\getVersion
static getVersion()
Get the current major version of Skin.
Definition: Skin.php:86
Title
Represents a title within MediaWiki.
Definition: Title.php:49
Skin\makeUrl
static makeUrl( $name, $urlaction='')
Definition: Skin.php:1311
wfMatchesDomainList
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
Definition: GlobalFunctions.php:860
$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:887
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
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:752
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:1357
CreditsAction
Definition: CreditsAction.php:31
Skin\makeMainPageUrl
static makeMainPageUrl( $urlaction='')
Definition: Skin.php:1267
Skin\getFooterLinks
getFooterLinks()
Get template representation of the footer containing site footer links as well as standard footer lin...
Definition: Skin.php:2581
$t
$t
Definition: testCompression.php:74
Skin\getCopyrightIcon
getCopyrightIcon()
Definition: Skin.php:930
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:232
Skin\getSkinName
getSkinName()
Definition: Skin.php:204
Skin\getAllowedSkins
static getAllowedSkins()
Fetch the list of user-selectable skins in regards to $wgSkipSkins.
Definition: Skin.php:112
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:44
Skin\initPage
initPage(OutputPage $out)
Definition: Skin.php:229
Skin\setRelevantTitle
setRelevantTitle( $t)
Definition: Skin.php:395
Skin\isResponsive
isResponsive()
Indicates if this skin is responsive.
Definition: Skin.php:217
Skin\makeSearchButton
makeSearchButton( $mode, $attrs=[])
Definition: Skin.php:2497
Skin\addToSidebarPlain
addToSidebarPlain(&$bar, $text)
Add content from plain text.
Definition: Skin.php:1739
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
Skin\$mRelevantUser
UserIdentity null false $mRelevantUser
Definition: Skin.php:66
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1325
Skin\setSearchPageTitle
setSearchPageTitle(Title $title)
Definition: Skin.php:2623
Skin\makeFooterIcon
makeFooterIcon( $icon, $withImage='withImage')
Renders a $wgFooterIcons icon according to the method's arguments.
Definition: Skin.php:1043
$type
$type
Definition: testCompression.php:52