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 
78  protected $action;
79 
87  public static function getVersion() {
88  return self::VERSION_MAJOR;
89  }
90 
107  private function getSectionsData(): array {
108  $sections = $this->getOutput()->getSections();
109  $data = [];
110  $parent = null;
111  $lastLevel = 0;
112  foreach ( $sections as $i => $section ) {
113  $nextSection = $sections[$i + 1] ?? null;
114  $level = $section['toclevel'];
115 
116  $data[] = $section + [
117  'has-subsections' => $nextSection !== null && $nextSection['toclevel'] > $level,
118  'is-last-item' => $nextSection === null || $nextSection['toclevel'] < $level,
119  ];
120  }
121 
122  return $data;
123  }
124 
143  public function getTemplateData() {
144  $title = $this->getTitle();
145  $out = $this->getOutput();
146  $user = $this->getUser();
147  $isMainPage = $title->isMainPage();
148  $blankedHeading = false;
149  // Heading can only be blanked on "views". It should
150  // still show on action=edit, diff pages and action=history
151  $isHeadingOverridable = $this->getAction() === 'view' &&
152  !$this->getRequest()->getRawVal( 'diff' );
153 
154  if ( $isMainPage && $isHeadingOverridable ) {
155  // Special casing for the main page to allow more freedom to editors, to
156  // design their home page differently. This came up in T290480.
157  // The parameter for logged in users is optional and may
158  // or may not be used.
159  $titleMsg = $user->isAnon() ?
160  $this->msg( 'mainpage-title' ) :
161  $this->msg( 'mainpage-title-loggedin', $user->getName() );
162 
163  // Treat as config and get from content language
164  $titleMsg->inContentLanguage();
165  $blankedHeading = $titleMsg->isBlank();
166  if ( !$titleMsg->isDisabled() ) {
167  $htmlTitle = $titleMsg->parse();
168  } else {
169  $htmlTitle = $out->getPageTitle();
170  }
171  } else {
172  $htmlTitle = $out->getPageTitle();
173  }
174 
175  $data = [
176  // raw HTML
177  'html-title-heading' => Html::rawElement(
178  'h1',
179  [
180  'id' => 'firstHeading',
181  'class' => 'firstHeading mw-first-heading',
182  'style' => $blankedHeading ? 'display: none' : null
183  ] + $this->getUserLanguageAttributes(),
184  $htmlTitle
185  ),
186  'html-title' => $htmlTitle,
187  // Array values - return data if TOC present T298796.
188  'array-sections' => $out->isTOCEnabled() ? $this->getSectionsData() : null,
189 
190  // Boolean values
191  'is-title-blank' => $blankedHeading, // @since 1.38
192  'is-anon' => $user->isAnon(),
193  'is-article' => $out->isArticle(),
194  'is-mainpage' => $isMainPage,
195  'is-specialpage' => $title->isSpecialPage(),
196  ];
197  return $data;
198  }
199 
209  public static function normalizeKey( $key ) {
211 
212  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
213  $skinNames = $skinFactory->getInstalledSkins();
214 
215  // Make keys lowercase for case-insensitive matching.
216  $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
217  $key = strtolower( $key );
218  $defaultSkin = strtolower( $wgDefaultSkin );
219  $fallbackSkin = strtolower( $wgFallbackSkin );
220 
221  if ( $key == '' || $key == 'default' ) {
222  // Don't return the default immediately;
223  // in a misconfiguration we need to fall back.
224  $key = $defaultSkin;
225  }
226 
227  if ( isset( $skinNames[$key] ) ) {
228  return $key;
229  }
230 
231  // Older versions of the software used a numeric setting
232  // in the user preferences.
233  $fallback = [
234  0 => $defaultSkin,
235  2 => 'cologneblue'
236  ];
237 
238  if ( isset( $fallback[$key] ) ) {
239  $key = $fallback[$key];
240  }
241 
242  if ( isset( $skinNames[$key] ) ) {
243  return $key;
244  } elseif ( isset( $skinNames[$defaultSkin] ) ) {
245  return $defaultSkin;
246  } else {
247  return $fallbackSkin;
248  }
249  }
250 
268  public function __construct( $options = null ) {
269  if ( is_string( $options ) ) {
270  $this->skinname = $options;
271  } elseif ( $options ) {
272  $name = $options['name'] ?? null;
273 
274  if ( !$name ) {
275  throw new SkinException( 'Skin name must be specified' );
276  }
277 
278  if ( isset( $options['link'] ) ) {
279  $this->defaultLinkOptions = $options['link'];
280  }
281  // Defaults are set in Skin::getOptions()
282  $this->options = $options;
283  $this->skinname = $name;
284  }
285  }
286 
290  public function getSkinName() {
291  return $this->skinname;
292  }
293 
303  public function isResponsive() {
304  $isSkinResponsiveCapable = $this->options['responsive'] ?? false;
305  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
306 
307  return $isSkinResponsiveCapable &&
308  $userOptionsLookup->getBoolOption( $this->getUser(), 'skin-responsive' );
309  }
310 
315  public function initPage( OutputPage $out ) {
316  $skinMetaTags = $this->getConfig()->get( 'SkinMetaTags' );
317  $this->preloadExistence();
318 
319  if ( $this->isResponsive() ) {
320  $out->addMeta(
321  'viewport',
322  'width=device-width, initial-scale=1.0, ' .
323  'user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0'
324  );
325  }
326 
327  $tags = [
328  'og:title' => $out->getHTMLTitle(),
329  'twitter:card' => 'summary_large_image',
330  'og:type' => 'website',
331  ];
332 
333  // Support sharing on platforms such as Facebook and Twitter
334  foreach ( $tags as $key => $value ) {
335  if ( in_array( $key, $skinMetaTags ) ) {
336  $out->addMeta( $key, $value );
337  }
338  }
339  }
340 
352  public function getDefaultModules() {
353  $out = $this->getOutput();
354  $user = $this->getUser();
355 
356  // Modules declared in the $modules literal are loaded
357  // for ALL users, on ALL pages, in ALL skins.
358  // Keep this list as small as possible!
359  $modules = [
360  // The 'styles' key sets render-blocking style modules
361  // Unlike other keys in $modules, this is an associative array
362  // where each key is its own group pointing to a list of modules
363  'styles' => [
364  'skin' => $this->options['styles'] ?? [],
365  'core' => [],
366  'content' => [],
367  'syndicate' => [],
368  ],
369  'core' => [
370  'site',
371  'mediawiki.page.ready',
372  ],
373  // modules that enhance the content in some way
374  'content' => [],
375  // modules relating to search functionality
376  'search' => [],
377  // Skins can register their own scripts
378  'skin' => $this->options['scripts'] ?? [],
379  // modules relating to functionality relating to watching an article
380  'watch' => [],
381  // modules which relate to the current users preferences
382  'user' => [],
383  // modules relating to RSS/Atom Feeds
384  'syndicate' => [],
385  ];
386 
387  // Preload jquery.tablesorter for mediawiki.page.ready
388  if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
389  $modules['content'][] = 'jquery.tablesorter';
390  $modules['styles']['content'][] = 'jquery.tablesorter.styles';
391  }
392 
393  // Preload jquery.makeCollapsible for mediawiki.page.ready
394  if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
395  $modules['content'][] = 'jquery.makeCollapsible';
396  $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
397  }
398 
399  // Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
400  // on every page is deprecated. Express a dependency instead.
401  if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
402  $modules['styles']['content'][] = 'mediawiki.ui.button';
403  }
404 
405  if ( $out->isTOCEnabled() ) {
406  $modules['content'][] = 'mediawiki.toc';
407  }
408 
409  $authority = $this->getAuthority();
410  if ( $authority->getUser()->isRegistered()
411  && $authority->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
412  && $this->getRelevantTitle()->canExist()
413  ) {
414  $modules['watch'][] = 'mediawiki.page.watch.ajax';
415  }
416 
417  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
418  if ( $userOptionsLookup->getBoolOption( $user, 'editsectiononrightclick' )
419  || ( $out->isArticle() && $userOptionsLookup->getOption( $user, 'editondblclick' ) )
420  ) {
421  $modules['user'][] = 'mediawiki.misc-authed-pref';
422  }
423 
424  if ( $out->isSyndicated() ) {
425  $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
426  }
427 
428  return $modules;
429  }
430 
434  protected function preloadExistence() {
435  $titles = [];
436 
437  // User/talk link
438  $user = $this->getUser();
439  if ( $user->isRegistered() ) {
440  $titles[] = $user->getUserPage();
441  $titles[] = $user->getTalkPage();
442  }
443 
444  // Check, if the page can hold some kind of content, otherwise do nothing
445  $title = $this->getRelevantTitle();
446  if ( $title->canExist() && $title->canHaveTalkPage() ) {
447  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
448  if ( $title->isTalkPage() ) {
449  $titles[] = $namespaceInfo->getSubjectPage( $title );
450  } else {
451  $titles[] = $namespaceInfo->getTalkPage( $title );
452  }
453  }
454 
455  // Footer links (used by SkinTemplate::prepareQuickTemplate)
456  if ( $this->getConfig()->get( 'FooterLinkCacheExpiry' ) <= 0 ) {
457  $titles = array_merge(
458  $titles,
459  array_filter( [
460  $this->footerLinkTitle( 'privacy', 'privacypage' ),
461  $this->footerLinkTitle( 'aboutsite', 'aboutpage' ),
462  $this->footerLinkTitle( 'disclaimers', 'disclaimerpage' ),
463  ] )
464  );
465  }
466 
467  $this->getHookRunner()->onSkinPreloadExistence( $titles, $this );
468 
469  if ( $titles ) {
470  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
471  $lb = $linkBatchFactory->newLinkBatch( $titles );
472  $lb->setCaller( __METHOD__ );
473  $lb->execute();
474  }
475  }
476 
481  public function setRelevantTitle( $t ) {
482  $this->mRelevantTitle = $t;
483  }
484 
495  public function getRelevantTitle() {
496  return $this->mRelevantTitle ?? $this->getTitle();
497  }
498 
503  public function setRelevantUser( ?UserIdentity $u ) {
504  $this->mRelevantUser = $u;
505  }
506 
516  public function getRelevantUser(): ?UserIdentity {
517  if ( $this->mRelevantUser === false ) {
518  $this->mRelevantUser = null; // false indicates we never attempted to load it.
519  $title = $this->getRelevantTitle();
520  if ( $title->hasSubjectNamespace( NS_USER ) ) {
521  $services = MediaWikiServices::getInstance();
522  $rootUser = $title->getRootText();
523  $userNameUtils = $services->getUserNameUtils();
524  if ( $userNameUtils->isIP( $rootUser ) ) {
525  $this->mRelevantUser = UserIdentityValue::newAnonymous( $rootUser );
526  } else {
527  $user = $services->getUserIdentityLookup()->getUserIdentityByName( $rootUser );
528  $this->mRelevantUser = $user && $user->isRegistered() ? $user : null;
529  }
530  }
531  }
532 
533  // The relevant user should only be set if it exists. However, if it exists but is hidden,
534  // and the viewer cannot see hidden users, this exposes the fact that the user exists;
535  // pretend like the user does not exist in such cases, by setting it to null. T120883
536  if ( $this->mRelevantUser && $this->mRelevantUser->isRegistered() ) {
537  $userBlock = MediaWikiServices::getInstance()
538  ->getBlockManager()
539  ->getUserBlock( $this->mRelevantUser, null, true );
540  if ( $userBlock && $userBlock->getHideName() &&
541  !$this->getAuthority()->isAllowed( 'hideuser' )
542  ) {
543  $this->mRelevantUser = null;
544  }
545  }
546 
547  return $this->mRelevantUser;
548  }
549 
553  abstract public function outputPage();
554 
560  public function getPageClasses( $title ) {
561  $numeric = 'ns-' . $title->getNamespace();
562 
563  if ( $title->isSpecialPage() ) {
564  $type = 'ns-special';
565  // T25315: provide a class based on the canonical special page name without subpages
566  list( $canonicalName ) = MediaWikiServices::getInstance()->getSpecialPageFactory()->
567  resolveAlias( $title->getDBkey() );
568  if ( $canonicalName ) {
569  $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
570  } else {
571  $type .= ' mw-invalidspecialpage';
572  }
573  } else {
574  if ( $title->isTalkPage() ) {
575  $type = 'ns-talk';
576  } else {
577  $type = 'ns-subject';
578  }
579  // T208315: add HTML class when the user can edit the page
580  if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
581  $type .= ' mw-editable';
582  }
583  }
584 
585  $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() );
586  $root = Sanitizer::escapeClass( 'rootpage-' . $title->getRootTitle()->getPrefixedText() );
587 
588  return "$numeric $type $name $root";
589  }
590 
595  public function getHtmlElementAttributes() {
596  $lang = $this->getLanguage();
597  return [
598  'lang' => $lang->getHtmlCode(),
599  'dir' => $lang->getDir(),
600  'class' => 'client-nojs',
601  ];
602  }
603 
607  public function getCategoryLinks() {
608  $out = $this->getOutput();
609  $allCats = $out->getCategoryLinks();
610  $title = $this->getTitle();
611  $services = MediaWikiServices::getInstance();
612  $linkRenderer = $services->getLinkRenderer();
613 
614  if ( $allCats === [] ) {
615  return '';
616  }
617 
618  $embed = "<li>";
619  $pop = "</li>";
620 
621  $s = '';
622  $colon = $this->msg( 'colon-separator' )->escaped();
623 
624  if ( !empty( $allCats['normal'] ) ) {
625  $t = $embed . implode( $pop . $embed, $allCats['normal'] ) . $pop;
626 
627  $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) );
628  $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
629  $pageCategoriesLinkTitle = Title::newFromText( $linkPage );
630  if ( $pageCategoriesLinkTitle ) {
631  $link = $linkRenderer->makeLink( $pageCategoriesLinkTitle, $msg->text() );
632  } else {
633  $link = $msg->escaped();
634  }
635  $s .= Html::rawElement(
636  'div',
637  [ 'id' => 'mw-normal-catlinks', 'class' => 'mw-normal-catlinks' ],
638  $link . $colon . Html::rawElement( 'ul', [], $t )
639  );
640  }
641 
642  # Hidden categories
643  if ( isset( $allCats['hidden'] ) ) {
644  $userOptionsLookup = $services->getUserOptionsLookup();
645 
646  if ( $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' ) ) {
647  $class = ' mw-hidden-cats-user-shown';
648  } elseif ( $title->inNamespace( NS_CATEGORY ) ) {
649  $class = ' mw-hidden-cats-ns-shown';
650  } else {
651  $class = ' mw-hidden-cats-hidden';
652  }
653 
654  $s .= Html::rawElement(
655  'div',
656  [ 'id' => 'mw-hidden-catlinks', 'class' => "mw-hidden-catlinks$class" ],
657  $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
658  $colon .
660  'ul',
661  [],
662  $embed . implode( $pop . $embed, $allCats['hidden'] ) . $pop
663  )
664  );
665  }
666 
667  return $s;
668  }
669 
673  public function getCategories() {
674  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
675  $showHiddenCats = $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' );
676 
677  $catlinks = $this->getCategoryLinks();
678  // Check what we're showing
679  $allCats = $this->getOutput()->getCategoryLinks();
680  $showHidden = $showHiddenCats || $this->getTitle()->inNamespace( NS_CATEGORY );
681 
682  $classes = [ 'catlinks' ];
683  if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
684  $classes[] = 'catlinks-allhidden';
685  }
686 
687  return Html::rawElement(
688  'div',
689  [ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
690  $catlinks
691  );
692  }
693 
708  protected function afterContentHook() {
709  $data = '';
710 
711  if ( $this->getHookRunner()->onSkinAfterContent( $data, $this ) ) {
712  // adding just some spaces shouldn't toggle the output
713  // of the whole <div/>, so we use trim() here
714  if ( trim( $data ) != '' ) {
715  // Doing this here instead of in the skins to
716  // ensure that the div has the same ID in all
717  // skins
718  $data = "<div id='mw-data-after-content'>\n" .
719  "\t$data\n" .
720  "</div>\n";
721  }
722  } else {
723  wfDebug( "Hook SkinAfterContent changed output processing." );
724  }
725 
726  return $data;
727  }
728 
734  public function bottomScripts() {
735  // TODO and the suckage continues. This function is really just a wrapper around
736  // OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
737  // up at some point
738  $chunks = [ $this->getOutput()->getBottomScripts() ];
739 
740  // Keep the hook appendage separate to preserve WrappedString objects.
741  // This enables BaseTemplate::getTrail() to merge them where possible.
742  $extraHtml = '';
743  $this->getHookRunner()->onSkinAfterBottomScripts( $this, $extraHtml );
744  if ( $extraHtml !== '' ) {
745  $chunks[] = $extraHtml;
746  }
747  return WrappedString::join( "\n", $chunks );
748  }
749 
757  public function printSource() {
758  $title = $this->getTitle();
759  $oldid = $this->getOutput()->getRevisionId();
760  if ( $oldid ) {
761  $canonicalUrl = $title->getCanonicalURL( 'oldid=' . $oldid );
762  $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
763  } else {
764  // oldid not available for non existing pages
765  $url = htmlspecialchars( wfExpandIRI( $title->getCanonicalURL() ) );
766  }
767 
768  return $this->msg( 'retrievedfrom' )
769  ->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
770  ->parse();
771  }
772 
776  public function getUndeleteLink() {
777  $action = $this->getRequest()->getRawVal( 'action', 'view' );
778  $title = $this->getTitle();
779  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
780 
781  if ( ( !$title->exists() || $action == 'history' ) &&
782  $this->getAuthority()->probablyCan( 'deletedhistory', $title )
783  ) {
784  $n = $title->getDeletedEditsCount();
785 
786  if ( $n ) {
787  if ( $this->getAuthority()->probablyCan( 'undelete', $title ) ) {
788  $msg = 'thisisdeleted';
789  } else {
790  $msg = 'viewdeleted';
791  }
792 
793  $subtitle = $this->msg( $msg )->rawParams(
794  $linkRenderer->makeKnownLink(
795  SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ),
796  $this->msg( 'restorelink' )->numParams( $n )->text() )
797  )->escaped();
798 
799  $links = [];
800  // Add link to page logs, unless we're on the history page (which
801  // already has one)
802  if ( $action !== 'history' ) {
803  $links[] = $linkRenderer->makeKnownLink(
804  SpecialPage::getTitleFor( 'Log' ),
805  $this->msg( 'viewpagelogs-lowercase' )->text(),
806  [],
807  [ 'page' => $title->getPrefixedText() ]
808  );
809  }
810 
811  // Allow extensions to add more links
812  $this->getHookRunner()->onUndeletePageToolLinks(
813  $this->getContext(), $linkRenderer, $links );
814 
815  if ( $links ) {
816  $subtitle .= ''
817  . $this->msg( 'word-separator' )->escaped()
818  . $this->msg( 'parentheses' )
819  ->rawParams( $this->getLanguage()->pipeList( $links ) )
820  ->escaped();
821  }
822 
823  return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
824  }
825  }
826 
827  return '';
828  }
829 
833  private function subPageSubtitleInternal() {
834  $services = MediaWikiServices::getInstance();
835  $linkRenderer = $services->getLinkRenderer();
836  $out = $this->getOutput();
837  $title = $out->getTitle();
838  $subpages = '';
839 
840  if ( !$this->getHookRunner()->onSkinSubPageSubtitle( $subpages, $this, $out ) ) {
841  return $subpages;
842  }
843 
844  $hasSubpages = $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() );
845  if ( !$out->isArticle() || !$hasSubpages ) {
846  return $subpages;
847  }
848 
849  $ptext = $title->getPrefixedText();
850  if ( strpos( $ptext, '/' ) !== false ) {
851  $links = explode( '/', $ptext );
852  array_pop( $links );
853  $count = 0;
854  $growingLink = '';
855  $display = '';
856  $lang = $this->getLanguage();
857 
858  foreach ( $links as $link ) {
859  $growingLink .= $link;
860  $display .= $link;
861  $linkObj = Title::newFromText( $growingLink );
862 
863  if ( $linkObj && $linkObj->isKnown() ) {
864  $getlink = $linkRenderer->makeKnownLink( $linkObj, $display );
865 
866  $count++;
867 
868  if ( $count > 1 ) {
869  $subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
870  } else {
871  $subpages .= '&lt; ';
872  }
873 
874  $subpages .= $getlink;
875  $display = '';
876  } else {
877  $display .= '/';
878  }
879  $growingLink .= '/';
880  }
881  }
882 
883  return $subpages;
884  }
885 
890  public function getCopyright( $type = 'detect' ) {
891  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
892  if ( $type == 'detect' ) {
893  if ( !$this->getOutput()->isRevisionCurrent()
894  && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
895  ) {
896  $type = 'history';
897  } else {
898  $type = 'normal';
899  }
900  }
901 
902  if ( $type == 'history' ) {
903  $msg = 'history_copyright';
904  } else {
905  $msg = 'copyright';
906  }
907 
908  $config = $this->getConfig();
909 
910  if ( $config->get( 'RightsPage' ) ) {
911  $title = Title::newFromText( $config->get( 'RightsPage' ) );
912  $link = $linkRenderer->makeKnownLink(
913  $title, new HtmlArmor( $config->get( 'RightsText' ) ?: $title->getText() )
914  );
915  } elseif ( $config->get( 'RightsUrl' ) ) {
916  $link = Linker::makeExternalLink( $config->get( 'RightsUrl' ), $config->get( 'RightsText' ) );
917  } elseif ( $config->get( 'RightsText' ) ) {
918  $link = $config->get( 'RightsText' );
919  } else {
920  # Give up now
921  return '';
922  }
923 
924  // Allow for site and per-namespace customization of copyright notice.
925  $this->getHookRunner()->onSkinCopyrightFooter( $this->getTitle(), $type, $msg, $link );
926 
927  return $this->msg( $msg )->rawParams( $link )->text();
928  }
929 
934  protected function getCopyrightIcon() {
935  wfDeprecated( __METHOD__, '1.37' );
936  return BaseTemplate::getCopyrightIconHTML( $this->getConfig(), $this );
937  }
938 
944  protected function getPoweredBy() {
945  wfDeprecated( __METHOD__, '1.37' );
946  $text = BaseTemplate::getPoweredByHTML( $this->getConfig() );
947  $this->getHookRunner()->onSkinGetPoweredBy( $text, $this );
948  return $text;
949  }
950 
956  protected function lastModified() {
957  $timestamp = $this->getOutput()->getRevisionTimestamp();
958  $user = $this->getUser();
959  $language = $this->getLanguage();
960 
961  # No cached timestamp, load it from the database
962  if ( $timestamp === null ) {
963  $revId = $this->getOutput()->getRevisionId();
964  if ( $revId !== null ) {
965  $timestamp = MediaWikiServices::getInstance()
966  ->getRevisionLookup()
967  ->getTimestampFromId( $revId );
968  }
969  }
970 
971  if ( $timestamp ) {
972  $d = $language->userDate( $timestamp, $user );
973  $t = $language->userTime( $timestamp, $user );
974  $s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->parse();
975  } else {
976  $s = '';
977  }
978 
979  if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getLaggedReplicaMode() ) {
980  $s .= ' <strong>' . $this->msg( 'laggedreplicamode' )->parse() . '</strong>';
981  }
982 
983  return $s;
984  }
985 
990  public function logoText( $align = '' ) {
991  if ( $align != '' ) {
992  $a = " style='float: {$align};'";
993  } else {
994  $a = '';
995  }
996 
997  $mp = $this->msg( 'mainpage' )->escaped();
998  $url = htmlspecialchars( Title::newMainPage()->getLocalURL() );
999 
1000  $logourl = ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() )[ '1x' ];
1001  return "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
1002  }
1003 
1012  public function makeFooterIcon( $icon, $withImage = 'withImage' ) {
1013  if ( is_string( $icon ) ) {
1014  $html = $icon;
1015  } else { // Assuming array
1016  $url = $icon['url'] ?? null;
1017  unset( $icon['url'] );
1018  if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
1019  // Lazy-load footer icons, since they're not part of the printed view.
1020  $icon['loading'] = 'lazy';
1021  // do this the lazy way, just pass icon data as an attribute array
1022  $html = Html::element( 'img', $icon );
1023  } else {
1024  $html = htmlspecialchars( $icon['alt'] ?? '' );
1025  }
1026  if ( $url ) {
1027  $html = Html::rawElement( 'a',
1028  [ 'href' => $url, 'target' => $this->getConfig()->get( 'ExternalLinkTarget' ) ],
1029  $html );
1030  }
1031  }
1032  return $html;
1033  }
1034 
1052  public function footerLink( $desc, $page ) {
1053  $title = $this->footerLinkTitle( $desc, $page );
1054 
1055  if ( !$title ) {
1056  return '';
1057  }
1058 
1059  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1060  return $linkRenderer->makeKnownLink(
1061  $title,
1062  $this->msg( $desc )->text()
1063  );
1064  }
1065 
1071  private function footerLinkTitle( $desc, $page ) {
1072  // If the link description has been disabled in the default language,
1073  if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
1074  // then it is disabled, for all languages.
1075  return null;
1076  }
1077  // Otherwise, we display the link for the user, described in their
1078  // language (which may or may not be the same as the default language),
1079  // but we make the link target be the one site-wide page.
1080  $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
1081 
1082  return $title ?: null;
1083  }
1084 
1091  public function getSiteFooterLinks() {
1092  $callback = function () {
1093  return [
1094  'privacy' => $this->footerLink( 'privacy', 'privacypage' ),
1095  'about' => $this->footerLink( 'aboutsite', 'aboutpage' ),
1096  'disclaimer' => $this->footerLink( 'disclaimers', 'disclaimerpage' )
1097  ];
1098  };
1099 
1100  $services = MediaWikiServices::getInstance();
1101  $msgCache = $services->getMessageCache();
1102  $wanCache = $services->getMainWANObjectCache();
1103  $config = $this->getConfig();
1104 
1105  return ( $config->get( 'FooterLinkCacheExpiry' ) > 0 )
1106  ? $wanCache->getWithSetCallback(
1107  $wanCache->makeKey( 'footer-links' ),
1108  $config->get( 'FooterLinkCacheExpiry' ),
1109  $callback,
1110  [
1111  'checkKeys' => [
1112  // Unless there is both no exact $code override nor an i18n definition
1113  // in the software, the only MediaWiki page to check is for $code.
1114  $msgCache->getCheckKey( $this->getLanguage()->getCode() )
1115  ],
1116  'lockTSE' => 30
1117  ]
1118  )
1119  : $callback();
1120  }
1121 
1129  public function editUrlOptions() {
1130  $options = [ 'action' => 'edit' ];
1131  $out = $this->getOutput();
1132 
1133  if ( !$out->isRevisionCurrent() ) {
1134  $options['oldid'] = intval( $out->getRevisionId() );
1135  }
1136 
1137  return $options;
1138  }
1139 
1144  public function showEmailUser( $id ) {
1145  if ( $id instanceof UserIdentity ) {
1146  $targetUser = User::newFromIdentity( $id );
1147  } else {
1148  $targetUser = User::newFromId( $id );
1149  }
1150 
1151  # The sending user must have a confirmed email address and the receiving
1152  # user must accept emails from the sender.
1153  return $this->getUser()->canSendEmail()
1154  && SpecialEmailUser::validateTarget( $targetUser, $this->getUser() ) === '';
1155  }
1156 
1170  public function getSkinStylePath( $name ) {
1171  wfDeprecated( __METHOD__, '1.36' );
1172 
1173  if ( $this->stylename === null ) {
1174  $class = static::class;
1175  throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
1176  }
1177 
1178  return $this->getConfig()->get( 'StylePath' ) . "/{$this->stylename}/$name";
1179  }
1180 
1181  /* these are used extensively in SkinTemplate, but also some other places */
1182 
1187  public static function makeMainPageUrl( $urlaction = '' ) {
1189 
1190  return $title->getLinkURL( $urlaction );
1191  }
1192 
1204  public static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1206  if ( $proto === null ) {
1207  return $title->getLocalURL( $urlaction );
1208  } else {
1209  return $title->getFullURL( $urlaction, false, $proto );
1210  }
1211  }
1212 
1219  public static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1220  $title = SpecialPage::getSafeTitleFor( $name, $subpage );
1221  return $title->getLocalURL( $urlaction );
1222  }
1223 
1230  public static function makeInternalOrExternalUrl( $name ) {
1231  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
1232  return $name;
1233  } else {
1234  $title = Title::newFromText( $name );
1235  self::checkTitle( $title, $name );
1236  return $title->getLocalURL();
1237  }
1238  }
1239 
1246  protected static function makeUrlDetails( $name, $urlaction = '' ) {
1247  $title = Title::newFromText( $name );
1248  self::checkTitle( $title, $name );
1249 
1250  return [
1251  'href' => $title->getLocalURL( $urlaction ),
1252  'exists' => $title->isKnown(),
1253  ];
1254  }
1255 
1262  protected static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1263  $title = Title::newFromText( $name );
1264  self::checkTitle( $title, $name );
1265 
1266  return [
1267  'href' => $title->getLocalURL( $urlaction ),
1268  'exists' => true
1269  ];
1270  }
1271 
1278  public static function checkTitle( &$title, $name ) {
1279  if ( !is_object( $title ) ) {
1280  $title = Title::newFromText( $name );
1281  if ( !is_object( $title ) ) {
1282  $title = Title::newFromText( '--error: link target missing--' );
1283  }
1284  }
1285  }
1286 
1295  public function mapInterwikiToLanguage( $code ) {
1296  $map = $this->getConfig()->get( 'InterlanguageLinkCodeMap' );
1297  return $map[ $code ] ?? $code;
1298  }
1299 
1308  public function getLanguages() {
1309  if ( $this->getConfig()->get( 'HideInterlanguageLinks' ) ) {
1310  return [];
1311  }
1312  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1313 
1314  $userLang = $this->getLanguage();
1315  $languageLinks = [];
1316  $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1317 
1318  foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
1319  $class = 'interlanguage-link interwiki-' . explode( ':', $languageLinkText, 2 )[0];
1320 
1321  $languageLinkTitle = Title::newFromText( $languageLinkText );
1322  if ( !$languageLinkTitle ) {
1323  continue;
1324  }
1325 
1326  $ilInterwikiCode = $this->mapInterwikiToLanguage( $languageLinkTitle->getInterwiki() );
1327 
1328  $ilLangName = $langNameUtils->getLanguageName( $ilInterwikiCode );
1329 
1330  if ( strval( $ilLangName ) === '' ) {
1331  $ilDisplayTextMsg = $this->msg( "interlanguage-link-$ilInterwikiCode" );
1332  if ( !$ilDisplayTextMsg->isDisabled() ) {
1333  // Use custom MW message for the display text
1334  $ilLangName = $ilDisplayTextMsg->text();
1335  } else {
1336  // Last resort: fallback to the language link target
1337  $ilLangName = $languageLinkText;
1338  }
1339  } else {
1340  // Use the language autonym as display text
1341  $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
1342  }
1343 
1344  // CLDR extension or similar is required to localize the language name;
1345  // otherwise we'll end up with the autonym again.
1346  $ilLangLocalName = $langNameUtils->getLanguageName(
1347  $ilInterwikiCode,
1348  $userLang->getCode()
1349  );
1350 
1351  $languageLinkTitleText = $languageLinkTitle->getText();
1352  if ( $ilLangLocalName === '' ) {
1353  $ilFriendlySiteName = $this->msg( "interlanguage-link-sitename-$ilInterwikiCode" );
1354  if ( !$ilFriendlySiteName->isDisabled() ) {
1355  if ( $languageLinkTitleText === '' ) {
1356  $ilTitle = $this->msg(
1357  'interlanguage-link-title-nonlangonly',
1358  $ilFriendlySiteName->text()
1359  )->text();
1360  } else {
1361  $ilTitle = $this->msg(
1362  'interlanguage-link-title-nonlang',
1363  $languageLinkTitleText,
1364  $ilFriendlySiteName->text()
1365  )->text();
1366  }
1367  } else {
1368  // we have nothing friendly to put in the title, so fall back to
1369  // displaying the interlanguage link itself in the title text
1370  // (similar to what is done in page content)
1371  $ilTitle = $languageLinkTitle->getInterwiki() .
1372  ":$languageLinkTitleText";
1373  }
1374  } elseif ( $languageLinkTitleText === '' ) {
1375  $ilTitle = $this->msg(
1376  'interlanguage-link-title-langonly',
1377  $ilLangLocalName
1378  )->text();
1379  } else {
1380  $ilTitle = $this->msg(
1381  'interlanguage-link-title',
1382  $languageLinkTitleText,
1383  $ilLangLocalName
1384  )->text();
1385  }
1386 
1387  $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
1388  $languageLink = [
1389  'href' => $languageLinkTitle->getFullURL(),
1390  'text' => $ilLangName,
1391  'title' => $ilTitle,
1392  'class' => $class,
1393  'link-class' => 'interlanguage-link-target',
1394  'lang' => $ilInterwikiCodeBCP47,
1395  'hreflang' => $ilInterwikiCodeBCP47,
1396  ];
1397  $hookContainer->run(
1398  'SkinTemplateGetLanguageLink',
1399  [ &$languageLink, $languageLinkTitle, $this->getTitle(), $this->getOutput() ],
1400  []
1401  );
1402  $languageLinks[] = $languageLink;
1403  }
1404 
1405  return $languageLinks;
1406  }
1407 
1414  protected function buildNavUrls() {
1415  $out = $this->getOutput();
1416  $title = $this->getTitle();
1417  $thispage = $title->getPrefixedDBkey();
1418  $uploadNavigationUrl = $this->getConfig()->get( 'UploadNavigationUrl' );
1419 
1420  $nav_urls = [];
1421  $nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1422  if ( $uploadNavigationUrl ) {
1423  $nav_urls['upload'] = [ 'href' => $uploadNavigationUrl ];
1424  } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
1425  $nav_urls['upload'] = [ 'href' => self::makeSpecialUrl( 'Upload' ) ];
1426  } else {
1427  $nav_urls['upload'] = false;
1428  }
1429  $nav_urls['specialpages'] = [ 'href' => self::makeSpecialUrl( 'Specialpages' ) ];
1430 
1431  $nav_urls['print'] = false;
1432  $nav_urls['permalink'] = false;
1433  $nav_urls['info'] = false;
1434  $nav_urls['whatlinkshere'] = false;
1435  $nav_urls['recentchangeslinked'] = false;
1436  $nav_urls['contributions'] = false;
1437  $nav_urls['log'] = false;
1438  $nav_urls['blockip'] = false;
1439  $nav_urls['mute'] = false;
1440  $nav_urls['emailuser'] = false;
1441  $nav_urls['userrights'] = false;
1442 
1443  // A print stylesheet is attached to all pages, but nobody ever
1444  // figures that out. :) Add a link...
1445  if ( !$out->isPrintable() && ( $out->isArticle() || $title->isSpecialPage() ) ) {
1446  $nav_urls['print'] = [
1447  'text' => $this->msg( 'printableversion' )->text(),
1448  'href' => 'javascript:print();'
1449  ];
1450  }
1451 
1452  if ( $out->isArticle() ) {
1453  // Also add a "permalink" while we're at it
1454  $revid = $out->getRevisionId();
1455  if ( $revid ) {
1456  $nav_urls['permalink'] = [
1457  'text' => $this->msg( 'permalink' )->text(),
1458  'href' => $title->getLocalURL( "oldid=$revid" )
1459  ];
1460  }
1461  }
1462 
1463  if ( $out->isArticleRelated() ) {
1464  $nav_urls['whatlinkshere'] = [
1465  'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $thispage )->getLocalURL()
1466  ];
1467 
1468  $nav_urls['info'] = [
1469  'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1470  'href' => $title->getLocalURL( "action=info" )
1471  ];
1472 
1473  if ( $title->exists() || $title->inNamespace( NS_CATEGORY ) ) {
1474  $nav_urls['recentchangeslinked'] = [
1475  'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $thispage )->getLocalURL()
1476  ];
1477  }
1478  }
1479 
1480  $user = $this->getRelevantUser();
1481 
1482  if ( $user ) {
1483  $rootUser = $user->getName();
1484 
1485  $nav_urls['contributions'] = [
1486  'text' => $this->msg( 'tool-link-contributions', $rootUser )->text(),
1487  'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1488  'tooltip-params' => [ $rootUser ],
1489  ];
1490 
1491  $nav_urls['log'] = [
1492  'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
1493  ];
1494 
1495  if ( $this->getAuthority()->isAllowed( 'block' ) ) {
1496  $nav_urls['blockip'] = [
1497  'text' => $this->msg( 'blockip', $rootUser )->text(),
1498  'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser )
1499  ];
1500  }
1501 
1502  if ( $this->showEmailUser( $user ) ) {
1503  $nav_urls['emailuser'] = [
1504  'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1505  'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1506  'tooltip-params' => [ $rootUser ],
1507  ];
1508  }
1509 
1510  if ( $user->isRegistered() ) {
1511  if ( $this->getUser()->isRegistered() && $this->getConfig()->get( 'EnableSpecialMute' ) ) {
1512  $nav_urls['mute'] = [
1513  'text' => $this->msg( 'mute-preferences' )->text(),
1514  'href' => self::makeSpecialUrlSubpage( 'Mute', $rootUser )
1515  ];
1516  }
1517 
1518  $sur = new UserrightsPage;
1519  $sur->setContext( $this->getContext() );
1520  $canChange = $sur->userCanChangeRights( $user );
1521  $delimiter = $this->getConfig()->get( 'UserrightsInterwikiDelimiter' );
1522  if ( str_contains( $rootUser, $delimiter ) ) {
1523  // Username contains interwiki delimiter, link it via the
1524  // #{userid} syntax. (T260222)
1525  $linkArgs = [ false, [ 'user' => "#{$user->getId()}" ] ];
1526  } else {
1527  $linkArgs = [ $rootUser ];
1528  }
1529  $nav_urls['userrights'] = [
1530  'text' => $this->msg(
1531  $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly',
1532  $rootUser
1533  )->text(),
1534  'href' => self::makeSpecialUrlSubpage( 'Userrights', ...$linkArgs )
1535  ];
1536  }
1537  }
1538 
1539  return $nav_urls;
1540  }
1541 
1547  final protected function buildFeedUrls() {
1548  $feeds = [];
1549  $out = $this->getOutput();
1550  if ( $out->isSyndicated() ) {
1551  foreach ( $out->getSyndicationLinks() as $format => $link ) {
1552  $feeds[$format] = [
1553  // Messages: feed-atom, feed-rss
1554  'text' => $this->msg( "feed-$format" )->text(),
1555  'href' => $link
1556  ];
1557  }
1558  }
1559  return $feeds;
1560  }
1561 
1586  public function buildSidebar() {
1587  $services = MediaWikiServices::getInstance();
1588  $callback = function ( $old = null, &$ttl = null ) {
1589  $bar = [];
1590  $this->addToSidebar( $bar, 'sidebar' );
1591  $this->getHookRunner()->onSkinBuildSidebar( $this, $bar );
1592  $msgCache = MediaWikiServices::getInstance()->getMessageCache();
1593  if ( $msgCache->isDisabled() ) {
1594  $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
1595  }
1596 
1597  return $bar;
1598  };
1599 
1600  $msgCache = $services->getMessageCache();
1601  $wanCache = $services->getMainWANObjectCache();
1602  $config = $this->getConfig();
1603  $languageCode = $this->getLanguage()->getCode();
1604 
1605  $sidebar = $config->get( 'EnableSidebarCache' )
1606  ? $wanCache->getWithSetCallback(
1607  $wanCache->makeKey( 'sidebar', $languageCode ),
1608  $config->get( 'SidebarCacheExpiry' ),
1609  $callback,
1610  [
1611  'checkKeys' => [
1612  // Unless there is both no exact $code override nor an i18n definition
1613  // in the software, the only MediaWiki page to check is for $code.
1614  $msgCache->getCheckKey( $languageCode )
1615  ],
1616  'lockTSE' => 30
1617  ]
1618  )
1619  : $callback();
1620 
1621  $sidebar['TOOLBOX'] = $this->makeToolbox(
1622  $this->buildNavUrls(),
1623  $this->buildFeedUrls()
1624  );
1625  $sidebar['LANGUAGES'] = $this->getLanguages();
1626  // Apply post-processing to the cached value
1627  $this->getHookRunner()->onSidebarBeforeOutput( $this, $sidebar );
1628 
1629  return $sidebar;
1630  }
1631 
1641  public function addToSidebar( &$bar, $message ) {
1642  $this->addToSidebarPlain( $bar, $this->msg( $message )->inContentLanguage()->plain() );
1643  }
1644 
1652  public function addToSidebarPlain( &$bar, $text ) {
1653  $lines = explode( "\n", $text );
1654 
1655  $heading = '';
1656  $config = $this->getConfig();
1657  $messageTitle = $config->get( 'EnableSidebarCache' )
1658  ? Title::newMainPage() : $this->getTitle();
1659  $messageCache = MediaWikiServices::getInstance()->getMessageCache();
1660 
1661  foreach ( $lines as $line ) {
1662  if ( strpos( $line, '*' ) !== 0 ) {
1663  continue;
1664  }
1665  $line = rtrim( $line, "\r" ); // for Windows compat
1666 
1667  if ( strpos( $line, '**' ) !== 0 ) {
1668  $heading = trim( $line, '* ' );
1669  if ( !array_key_exists( $heading, $bar ) ) {
1670  $bar[$heading] = [];
1671  }
1672  } else {
1673  $line = trim( $line, '* ' );
1674 
1675  if ( strpos( $line, '|' ) !== false ) {
1676  $line = $messageCache->transform( $line, false, null, $messageTitle );
1677  $line = array_map( 'trim', explode( '|', $line, 2 ) );
1678  if ( count( $line ) !== 2 ) {
1679  // Second check, could be hit by people doing
1680  // funky stuff with parserfuncs... (T35321)
1681  continue;
1682  }
1683 
1684  $extraAttribs = [];
1685 
1686  $msgLink = $this->msg( $line[0] )->page( $messageTitle )->inContentLanguage();
1687  if ( $msgLink->exists() ) {
1688  $link = $msgLink->text();
1689  if ( $link == '-' ) {
1690  continue;
1691  }
1692  } else {
1693  $link = $line[0];
1694  }
1695  $msgText = $this->msg( $line[1] )->page( $messageTitle );
1696  if ( $msgText->exists() ) {
1697  $text = $msgText->text();
1698  } else {
1699  $text = $line[1];
1700  }
1701 
1702  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
1703  $href = $link;
1704 
1705  // Parser::getExternalLinkAttribs won't work here because of the Namespace things
1706  if ( $config->get( 'NoFollowLinks' ) &&
1707  !wfMatchesDomainList( $href, $config->get( 'NoFollowDomainExceptions' ) )
1708  ) {
1709  $extraAttribs['rel'] = 'nofollow';
1710  }
1711 
1712  if ( $config->get( 'ExternalLinkTarget' ) ) {
1713  $extraAttribs['target'] = $config->get( 'ExternalLinkTarget' );
1714  }
1715  } else {
1716  $title = Title::newFromText( $link );
1717 
1718  if ( $title ) {
1719  $title = $title->fixSpecialName();
1720  $href = $title->getLinkURL();
1721  } else {
1722  $href = 'INVALID-TITLE';
1723  }
1724  }
1725 
1726  $bar[$heading][] = array_merge( [
1727  'text' => $text,
1728  'href' => $href,
1729  'id' => Sanitizer::escapeIdForAttribute( 'n-' . strtr( $line[1], ' ', '-' ) ),
1730  'active' => false,
1731  ], $extraAttribs );
1732  }
1733  }
1734  }
1735 
1736  return $bar;
1737  }
1738 
1744  public function getNewtalks() {
1745  $newMessagesAlert = '';
1746  $user = $this->getUser();
1747  $services = MediaWikiServices::getInstance();
1748  $linkRenderer = $services->getLinkRenderer();
1749  $userHasNewMessages = $services->getTalkPageNotificationManager()
1750  ->userHasNewMessages( $user );
1751  $timestamp = $services->getTalkPageNotificationManager()
1752  ->getLatestSeenMessageTimestamp( $user );
1753  $newtalks = !$userHasNewMessages ? [] : [
1754  [
1755  // TODO: Deprecate adding wiki and link to array and redesign GetNewMessagesAlert hook
1756  'wiki' => WikiMap::getCurrentWikiId(),
1757  'link' => $user->getTalkPage()->getLocalURL(),
1758  'rev' => $timestamp ? $services->getRevisionLookup()
1759  ->getRevisionByTimestamp( $user->getTalkPage(), $timestamp ) : null
1760  ]
1761  ];
1762  $out = $this->getOutput();
1763 
1764  // Allow extensions to disable or modify the new messages alert
1765  if ( !$this->getHookRunner()->onGetNewMessagesAlert(
1766  $newMessagesAlert, $newtalks, $user, $out )
1767  ) {
1768  return '';
1769  }
1770  if ( $newMessagesAlert ) {
1771  return $newMessagesAlert;
1772  }
1773 
1774  if ( $newtalks !== [] ) {
1775  $uTalkTitle = $user->getTalkPage();
1776  $lastSeenRev = $newtalks[0]['rev'];
1777  $numAuthors = 0;
1778  if ( $lastSeenRev !== null ) {
1779  $plural = true; // Default if we have a last seen revision: if unknown, use plural
1780  $revStore = $services->getRevisionStore();
1781  $latestRev = $revStore->getRevisionByTitle(
1782  $uTalkTitle,
1783  0,
1784  RevisionLookup::READ_NORMAL
1785  );
1786  if ( $latestRev !== null ) {
1787  // Singular if only 1 unseen revision, plural if several unseen revisions.
1788  $plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1789  $numAuthors = $revStore->countAuthorsBetween(
1790  $uTalkTitle->getArticleID(),
1791  $lastSeenRev,
1792  $latestRev,
1793  null,
1794  10,
1795  RevisionStore::INCLUDE_NEW
1796  );
1797  }
1798  } else {
1799  // Singular if no revision -> diff link will show latest change only in any case
1800  $plural = false;
1801  }
1802  $plural = $plural ? 999 : 1;
1803  // 999 signifies "more than one revision". We don't know how many, and even if we did,
1804  // the number of revisions or authors is not necessarily the same as the number of
1805  // "messages".
1806  $newMessagesLink = $linkRenderer->makeKnownLink(
1807  $uTalkTitle,
1808  $this->msg( 'newmessageslinkplural' )->params( $plural )->text(),
1809  [],
1810  $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
1811  );
1812 
1813  $newMessagesDiffLink = $linkRenderer->makeKnownLink(
1814  $uTalkTitle,
1815  $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->text(),
1816  [],
1817  $lastSeenRev !== null
1818  ? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1819  : [ 'diff' => 'cur' ]
1820  );
1821 
1822  if ( $numAuthors >= 1 && $numAuthors <= 10 ) {
1823  $newMessagesAlert = $this->msg(
1824  'youhavenewmessagesfromusers',
1825  $newMessagesLink,
1826  $newMessagesDiffLink
1827  )->numParams( $numAuthors, $plural );
1828  } else {
1829  // $numAuthors === 11 signifies "11 or more" ("more than 10")
1830  $newMessagesAlert = $this->msg(
1831  $numAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
1832  $newMessagesLink,
1833  $newMessagesDiffLink
1834  )->numParams( $plural );
1835  }
1836  $newMessagesAlert = $newMessagesAlert->text();
1837  // Disable CDN cache
1838  $out->setCdnMaxage( 0 );
1839  }
1840 
1841  return $newMessagesAlert;
1842  }
1843 
1851  private function getCachedNotice( $name ) {
1852  $config = $this->getConfig();
1853 
1854  if ( $name === 'default' ) {
1855  // special case
1856  $notice = $config->get( 'SiteNotice' );
1857  if ( empty( $notice ) ) {
1858  return false;
1859  }
1860  } else {
1861  $msg = $this->msg( $name )->inContentLanguage();
1862  if ( $msg->isBlank() ) {
1863  return '';
1864  } elseif ( $msg->isDisabled() ) {
1865  return false;
1866  }
1867  $notice = $msg->plain();
1868  }
1869 
1870  $services = MediaWikiServices::getInstance();
1871  $cache = $services->getMainWANObjectCache();
1872  $parsed = $cache->getWithSetCallback(
1873  // Use the extra hash appender to let eg SSL variants separately cache
1874  // Key is verified with md5 hash of unparsed wikitext
1875  $cache->makeKey( $name, $config->get( 'RenderHashAppend' ), md5( $notice ) ),
1876  // TTL in seconds
1877  600,
1878  function () use ( $notice ) {
1879  return $this->getOutput()->parseAsInterface( $notice );
1880  }
1881  );
1882 
1883  $contLang = $services->getContentLanguage();
1884  return Html::rawElement(
1885  'div',
1886  [
1887  'id' => 'localNotice',
1888  'lang' => $contLang->getHtmlCode(),
1889  'dir' => $contLang->getDir()
1890  ],
1891  $parsed
1892  );
1893  }
1894 
1898  public function getSiteNotice() {
1899  $siteNotice = '';
1900 
1901  if ( $this->getHookRunner()->onSiteNoticeBefore( $siteNotice, $this ) ) {
1902  if ( $this->getUser()->isRegistered() ) {
1903  $siteNotice = $this->getCachedNotice( 'sitenotice' );
1904  } else {
1905  $anonNotice = $this->getCachedNotice( 'anonnotice' );
1906  if ( $anonNotice === false ) {
1907  $siteNotice = $this->getCachedNotice( 'sitenotice' );
1908  } else {
1909  $siteNotice = $anonNotice;
1910  }
1911  }
1912  if ( $siteNotice === false ) {
1913  $siteNotice = $this->getCachedNotice( 'default' ) ?: '';
1914  }
1915  }
1916 
1917  $this->getHookRunner()->onSiteNoticeAfter( $siteNotice, $this );
1918  return $siteNotice;
1919  }
1920 
1933  public function doEditSectionLink( Title $nt, $section, $tooltip, Language $lang ) {
1934  // HTML generated here should probably have userlangattributes
1935  // added to it for LTR text on RTL pages
1936 
1937  $attribs = [];
1938  if ( $tooltip !== null ) {
1939  $attribs['title'] = $this->msg( 'editsectionhint' )->rawParams( $tooltip )
1940  ->inLanguage( $lang )->text();
1941  }
1942 
1943  $links = [
1944  'editsection' => [
1945  'text' => $this->msg( 'editsection' )->inLanguage( $lang )->text(),
1946  'targetTitle' => $nt,
1947  'attribs' => $attribs,
1948  'query' => [ 'action' => 'edit', 'section' => $section ]
1949  ]
1950  ];
1951 
1952  $this->getHookRunner()->onSkinEditSectionLinks( $this, $nt, $section, $tooltip, $links, $lang );
1953 
1954  $result = Html::openElement( 'span', [ 'class' => 'mw-editsection' ] );
1955  $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], '[' );
1956 
1957  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1958  $linksHtml = [];
1959  foreach ( $links as $k => $linkDetails ) {
1960  $linksHtml[] = $linkRenderer->makeKnownLink(
1961  $linkDetails['targetTitle'],
1962  $linkDetails['text'],
1963  $linkDetails['attribs'],
1964  $linkDetails['query']
1965  );
1966  }
1967 
1968  $result .= implode(
1970  'span',
1971  [ 'class' => 'mw-editsection-divider' ],
1972  $this->msg( 'pipe-separator' )->inLanguage( $lang )->escaped()
1973  ),
1974  $linksHtml
1975  );
1976 
1977  $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], ']' );
1978  $result .= Html::closeElement( 'span' );
1979  return $result;
1980  }
1981 
1991  public function makeToolbox( $navUrls, $feedUrls ) {
1992  $toolbox = [];
1993  if ( $navUrls['whatlinkshere'] ?? null ) {
1994  $toolbox['whatlinkshere'] = $navUrls['whatlinkshere'];
1995  $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
1996  }
1997  if ( $navUrls['recentchangeslinked'] ?? null ) {
1998  $toolbox['recentchangeslinked'] = $navUrls['recentchangeslinked'];
1999  $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
2000  $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
2001  $toolbox['recentchangeslinked']['rel'] = 'nofollow';
2002  }
2003  if ( $feedUrls ) {
2004  $toolbox['feeds']['id'] = 'feedlinks';
2005  $toolbox['feeds']['links'] = [];
2006  foreach ( $feedUrls as $key => $feed ) {
2007  $toolbox['feeds']['links'][$key] = $feed;
2008  $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
2009  $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
2010  $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
2011  $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
2012  }
2013  }
2014  foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
2015  'userrights', 'upload', 'specialpages' ] as $special
2016  ) {
2017  if ( $navUrls[$special] ?? null ) {
2018  $toolbox[$special] = $navUrls[$special];
2019  $toolbox[$special]['id'] = "t-$special";
2020  }
2021  }
2022  if ( $navUrls['print'] ?? null ) {
2023  $toolbox['print'] = $navUrls['print'];
2024  $toolbox['print']['id'] = 't-print';
2025  $toolbox['print']['rel'] = 'alternate';
2026  $toolbox['print']['msg'] = 'printableversion';
2027  }
2028  if ( $navUrls['permalink'] ?? null ) {
2029  $toolbox['permalink'] = $navUrls['permalink'];
2030  $toolbox['permalink']['id'] = 't-permalink';
2031  }
2032  if ( $navUrls['info'] ?? null ) {
2033  $toolbox['info'] = $navUrls['info'];
2034  $toolbox['info']['id'] = 't-info';
2035  }
2036 
2037  return $toolbox;
2038  }
2039 
2046  protected function getIndicatorsData( $indicators ) {
2047  $indicatorData = [];
2048  foreach ( $indicators as $id => $content ) {
2049  $indicatorData[] = [
2050  'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
2051  'class' => 'mw-indicator',
2052  'html' => $content,
2053  ];
2054  }
2055  return $indicatorData;
2056  }
2057 
2072  final public function getPersonalToolsForMakeListItem( $urls, $applyClassesToListItems = false ) {
2073  $personal_tools = [];
2074  foreach ( $urls as $key => $plink ) {
2075  # The class on a personal_urls item is meant to go on the <a> instead
2076  # of the <li> so we have to use a single item "links" array instead
2077  # of using most of the personal_url's keys directly.
2078  $ptool = [
2079  'links' => [
2080  [ 'single-id' => "pt-$key" ],
2081  ],
2082  'id' => "pt-$key",
2083  ];
2084  if ( $applyClassesToListItems && isset( $plink['class'] ) ) {
2085  $ptool['class'] = $plink['class'];
2086  }
2087  if ( isset( $plink['active'] ) ) {
2088  $ptool['active'] = $plink['active'];
2089  }
2090  // Set class for the link to link-class, when defined.
2091  // This allows newer notifications content navigation to retain their classes
2092  // when merged back into the personal tools.
2093  // Doing this here allows the loop below to overwrite the class if defined directly.
2094  if ( isset( $plink['link-class'] ) ) {
2095  $ptool['links'][0]['class'] = $plink['link-class'];
2096  }
2097  $props = [
2098  'href',
2099  'text',
2100  'dir',
2101  'data',
2102  'exists',
2103  'data-mw'
2104  ];
2105  if ( !$applyClassesToListItems ) {
2106  $props[] = 'class';
2107  }
2108  foreach ( $props as $k ) {
2109  if ( isset( $plink[$k] ) ) {
2110  $ptool['links'][0][$k] = $plink[$k];
2111  }
2112  }
2113  $personal_tools[$key] = $ptool;
2114  }
2115  return $personal_tools;
2116  }
2117 
2177  final public function makeLink( $key, $item, $linkOptions = [] ) {
2178  $options = $linkOptions + $this->defaultLinkOptions;
2179  $text = $item['text'] ?? $this->msg( $item['msg'] ?? $key )->text();
2180 
2181  $html = htmlspecialchars( $text );
2182 
2183  if ( isset( $options['text-wrapper'] ) ) {
2184  $wrapper = $options['text-wrapper'];
2185  if ( isset( $wrapper['tag'] ) ) {
2186  $wrapper = [ $wrapper ];
2187  }
2188  while ( count( $wrapper ) > 0 ) {
2189  $element = array_pop( $wrapper );
2190  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
2191  $html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
2192  }
2193  }
2194 
2195  if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
2196  $attrs = $item;
2197  foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
2198  'tooltip-params', 'exists', 'link-html' ] as $k ) {
2199  unset( $attrs[$k] );
2200  }
2201 
2202  if ( isset( $attrs['data'] ) ) {
2203  foreach ( $attrs['data'] as $key => $value ) {
2204  $attrs[ 'data-' . $key ] = $value;
2205  }
2206  unset( $attrs[ 'data' ] );
2207  }
2208 
2209  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2210  $item['single-id'] = $item['id'];
2211  }
2212 
2213  $tooltipParams = [];
2214  if ( isset( $item['tooltip-params'] ) ) {
2215  $tooltipParams = $item['tooltip-params'];
2216  }
2217 
2218  if ( isset( $item['single-id'] ) ) {
2219  $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
2220 
2221  if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
2222  $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
2223  if ( $title !== false ) {
2224  $attrs['title'] = $title;
2225  }
2226  } else {
2228  $item['single-id'],
2229  $tooltipParams,
2230  $tooltipOption
2231  );
2232  if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
2233  $attrs['title'] = $tip['title'];
2234  }
2235  if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
2236  $attrs['accesskey'] = $tip['accesskey'];
2237  }
2238  }
2239  }
2240  if ( isset( $options['link-class'] ) ) {
2241  $attrs['class'] = $this->addClassToClassList( $attrs['class'] ?? [], $options['link-class'] );
2242  }
2243 
2244  if ( isset( $item['link-html'] ) ) {
2245  $html = $item['link-html'] . ' ' . $html;
2246  }
2247 
2248  $html = Html::rawElement( isset( $attrs['href'] )
2249  ? 'a'
2250  : $options['link-fallback'], $attrs, $html );
2251  }
2252 
2253  return $html;
2254  }
2255 
2290  final public function makeListItem( $key, $item, $options = [] ) {
2291  // In case this is still set from SkinTemplate, we don't want it to appear in
2292  // the HTML output (normally removed in SkinTemplate::buildContentActionUrls())
2293  unset( $item['redundant'] );
2294 
2295  if ( isset( $item['links'] ) ) {
2296  $links = [];
2297  foreach ( $item['links'] as $link ) {
2298  // Note: links will have identical label unless 'msg' is set on $link
2299  $links[] = $this->makeLink( $key, $link, $options );
2300  }
2301  $html = implode( ' ', $links );
2302  } else {
2303  $link = $item;
2304  // These keys are used by makeListItem and shouldn't be passed on to the link
2305  foreach ( [ 'id', 'class', 'active', 'tag', 'itemtitle' ] as $k ) {
2306  unset( $link[$k] );
2307  }
2308  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2309  // The id goes on the <li> not on the <a> for single links
2310  // but makeSidebarLink still needs to know what id to use when
2311  // generating tooltips and accesskeys.
2312  $link['single-id'] = $item['id'];
2313  }
2314  if ( isset( $link['link-class'] ) ) {
2315  // link-class should be set on the <a> itself,
2316  // so pass it in as 'class'
2317  $link['class'] = $link['link-class'];
2318  unset( $link['link-class'] );
2319  }
2320  $html = $this->makeLink( $key, $link, $options );
2321  }
2322 
2323  $attrs = [];
2324  foreach ( [ 'id', 'class' ] as $attr ) {
2325  if ( isset( $item[$attr] ) ) {
2326  $attrs[$attr] = $item[$attr];
2327  }
2328  }
2329  $attrs['class'] = $this->addClassToClassList( $attrs['class'] ?? [], 'mw-list-item' );
2330 
2331  if ( isset( $item['active'] ) && $item['active'] ) {
2332  // In the future, this should accept an array of classes, not a string
2333  $attrs['class'] = $this->addClassToClassList( $attrs['class'], 'active' );
2334  }
2335  if ( isset( $item['itemtitle'] ) ) {
2336  $attrs['title'] = $item['itemtitle'];
2337  }
2338  return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
2339  }
2340 
2349  private function addClassToClassList( $class, string $newClass ) {
2350  if ( is_array( $class ) ) {
2351  $class[] = $newClass;
2352  } else {
2353  $class .= ' ' . $newClass;
2354  $class = trim( $class );
2355  }
2356  return $class;
2357  }
2358 
2365  protected function getSearchInputAttributes( $attrs = [] ) {
2366  $autoCapHint = $this->getConfig()->get( 'CapitalLinks' );
2367  $realAttrs = [
2368  'type' => 'search',
2369  'name' => 'search',
2370  'placeholder' => $this->msg( 'searchsuggest-search' )->text(),
2371  'aria-label' => $this->msg( 'searchsuggest-search' )->text(),
2372  // T251664: Disable autocapitalization of input
2373  // method when using fully case-sensitive titles.
2374  'autocapitalize' => $autoCapHint ? 'sentences' : 'none',
2375  ];
2376 
2377  return array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
2378  }
2379 
2386  final public function makeSearchInput( $attrs = [] ) {
2387  return Html::element( 'input', $this->getSearchInputAttributes( $attrs ) );
2388  }
2389 
2397  final public function makeSearchButton( $mode, $attrs = [] ) {
2398  switch ( $mode ) {
2399  case 'go':
2400  case 'fulltext':
2401  $realAttrs = [
2402  'type' => 'submit',
2403  'name' => $mode,
2404  'value' => $this->msg( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
2405  ];
2406  $realAttrs = array_merge(
2407  $realAttrs,
2408  Linker::tooltipAndAccesskeyAttribs( "search-$mode" ),
2409  $attrs
2410  );
2411  return Html::element( 'input', $realAttrs );
2412  case 'image':
2413  $buttonAttrs = [
2414  'type' => 'submit',
2415  'name' => 'button',
2416  ];
2417  $buttonAttrs = array_merge(
2418  $buttonAttrs,
2419  Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ),
2420  $attrs
2421  );
2422  unset( $buttonAttrs['src'] );
2423  unset( $buttonAttrs['alt'] );
2424  unset( $buttonAttrs['width'] );
2425  unset( $buttonAttrs['height'] );
2426  $imgAttrs = [
2427  'src' => $attrs['src'],
2428  'alt' => $attrs['alt'] ?? $this->msg( 'searchbutton' )->text(),
2429  'width' => $attrs['width'] ?? null,
2430  'height' => $attrs['height'] ?? null,
2431  ];
2432  return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
2433  default:
2434  throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' );
2435  }
2436  }
2437 
2448  public function getAfterPortlet( string $name ): string {
2449  $html = '';
2450 
2451  $this->getHookRunner()->onSkinAfterPortlet( $this, $name, $html );
2452 
2453  return $html;
2454  }
2455 
2461  final public function prepareSubtitle() {
2462  $out = $this->getOutput();
2463  $subpagestr = $this->subPageSubtitleInternal();
2464  if ( $subpagestr !== '' ) {
2465  $subpagestr = Html::rawElement( 'span', [ 'class' => 'subpages' ], $subpagestr );
2466  }
2467  return $subpagestr . $out->getSubtitle();
2468  }
2469 
2481  protected function getFooterLinks(): array {
2482  $out = $this->getOutput();
2483  $title = $out->getTitle();
2484  $titleExists = $title->exists();
2485  $config = $this->getConfig();
2486  $maxCredits = $config->get( 'MaxCredits' );
2487  $showCreditsIfMax = $config->get( 'ShowCreditsIfMax' );
2488  $useCredits = $titleExists
2489  && $out->isArticle()
2490  && $out->isRevisionCurrent()
2491  && $maxCredits !== 0;
2492 
2494  if ( $useCredits ) {
2495  $article = Article::newFromWikiPage( $this->getWikiPage(), $this );
2496  $action = Action::factory( 'credits', $article, $this );
2497  }
2498 
2499  '@phan-var CreditsAction $action';
2500  $data = [
2501  'info' => [
2502  'lastmod' => !$useCredits ? $this->lastModified() : null,
2503  'numberofwatchingusers' => null,
2504  'credits' => $useCredits ?
2505  $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
2506  'copyright' => $titleExists &&
2507  $out->showsCopyright() ? $this->getCopyright() : null,
2508  ],
2509  'places' => $this->getSiteFooterLinks(),
2510  ];
2511  foreach ( $data as $key => $existingItems ) {
2512  $newItems = [];
2513  $this->getHookRunner()->onSkinAddFooterLinks( $this, $key, $newItems );
2514  $data[$key] = $existingItems + $newItems;
2515  }
2516  return $data;
2517  }
2518 
2526  protected function getJsConfigVars(): array {
2527  return [];
2528  }
2529 
2535  private function getUserLanguageAttributes() {
2536  $userLang = $this->getLanguage();
2537  $userLangCode = $userLang->getHtmlCode();
2538  $userLangDir = $userLang->getDir();
2539  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2540  if (
2541  $userLangCode !== $contLang->getHtmlCode() ||
2542  $userLangDir !== $contLang->getDir()
2543  ) {
2544  return [
2545  'lang' => $userLangCode,
2546  'dir' => $userLangDir,
2547  ];
2548  }
2549  return [];
2550  }
2551 
2557  final protected function prepareUserLanguageAttributes() {
2558  return Html::expandAttributes(
2559  $this->getUserLanguageAttributes()
2560  );
2561  }
2562 
2568  final protected function prepareUndeleteLink() {
2569  $undelete = $this->getUndeleteLink();
2570  return $undelete === '' ? null : '<span class="subpages">' . $undelete . '</span>';
2571  }
2572 
2579  final protected function getAction() {
2580  if ( $this->action ) {
2581  return $this->action;
2582  }
2583  $this->action = Action::getActionName( $this );
2584  return $this->action;
2585  }
2586 
2595  protected function wrapHTML( $title, $html ) {
2596  # An ID that includes the actual body text; without categories, contentSub, ...
2597  $realBodyAttribs = [
2598  'id' => 'mw-content-text',
2599  'class' => [
2600  'mw-body-content',
2601  ],
2602  ];
2603  $action = $this->getAction();
2604 
2605  # Add a mw-content-ltr/rtl class to be able to style based on text
2606  # direction when the content is different from the UI language (only
2607  # when viewing)
2608  # Most information on special pages and file pages is in user language,
2609  # rather than content language, so those will not get this
2610  if ( $action === 'view' &&
2611  ( !$title->inNamespaces( NS_SPECIAL, NS_FILE ) || $title->isRedirect() ) ) {
2612  $pageLang = $title->getPageViewLanguage();
2613  $realBodyAttribs['lang'] = $pageLang->getHtmlCode();
2614  $realBodyAttribs['dir'] = $pageLang->getDir();
2615  $realBodyAttribs['class'][] = 'mw-content-' . $pageLang->getDir();
2616  }
2617 
2618  return Html::rawElement( 'div', $realBodyAttribs, $html );
2619  }
2620 
2624  public function getSearchPageTitle(): Title {
2625  wfDeprecated( __METHOD__, '1.38 Use SpecialPage::newSearchPage' );
2626  return SpecialPage::newSearchPage( $this->getUser() );
2627  }
2628 
2633  public function setSearchPageTitle( Title $title ) {
2634  $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager();
2635  $user = $this->getUser();
2636  $currentTitle = $userOptionsManager->getOption( $user, 'search-special-page' );
2637  $newTitle = $title->getText();
2638  if ( $currentTitle !== $newTitle ) {
2639  $userOptionsManager->setOption( $user, 'search-special-page', $newTitle );
2640  }
2641  }
2642 
2650  final public function getOptions(): array {
2651  return $this->options + [
2652  // Whether the table of contents will be inserted on page views
2653  // See ParserOutput::getText() for the implementation logic
2654  'toc' => true,
2655  ];
2656  }
2657 
2672  public static function getPortletLinkOptions( ResourceLoaderContext $context ): array {
2673  $skinName = $context->getSkin();
2674  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2675  $options = $skinFactory->getSkinOptions( $skinName );
2676  $portletLinkOptions = $options['link'] ?? [];
2677  // Normalize link options to always have this key
2678  $portletLinkOptions += [ 'text-wrapper' => [] ];
2679  // Normalize text-wrapper to always be an array of arrays
2680  if ( isset( $portletLinkOptions['text-wrapper']['tag'] ) ) {
2681  $portletLinkOptions['text-wrapper'] = [ $portletLinkOptions['text-wrapper'] ];
2682  }
2683  return $portletLinkOptions;
2684  }
2685 }
Action\getActionName
static getActionName(IContextSource $context)
Get the action that will be executed, not necessarily the one passed passed through the "action" requ...
Definition: Action.php:108
Skin\prepareSubtitle
prepareSubtitle()
Prepare the subtitle of the page for output in the skin if one has been set.
Definition: Skin.php:2461
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:413
Skin\$skinname
string null $skinname
Definition: Skin.php:55
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:39
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
Skin\getAction
getAction()
Optimization.
Definition: Skin.php:2579
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:37
Skin\editUrlOptions
editUrlOptions()
Return URL options for the 'edit page' link.
Definition: Skin.php:1129
Skin\showEmailUser
showEmailUser( $id)
Definition: Skin.php:1144
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:636
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:377
Skin\makeUrlDetails
static makeUrlDetails( $name, $urlaction='')
these return an array with the 'href' and boolean 'exists'
Definition: Skin.php:1246
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
Skin\getSiteNotice
getSiteNotice()
Definition: Skin.php:1898
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:2177
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:1547
Skin\subPageSubtitleInternal
subPageSubtitleInternal()
Definition: Skin.php:833
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:1071
IContextSource\getSkin
getSkin()
Skin\getPoweredBy
getPoweredBy()
Gets the powered by MediaWiki icon.
Definition: Skin.php:944
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
$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:2072
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:89
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
$fallback
$fallback
Definition: MessagesAb.php:11
Skin\lastModified
lastModified()
Get the timestamp of the latest revision, formatted in user language.
Definition: Skin.php:956
Skin\getSiteFooterLinks
getSiteFooterLinks()
Gets the link to the wiki's privacy policy, about page, and disclaimer page.
Definition: Skin.php:1091
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:1278
Skin\mapInterwikiToLanguage
mapInterwikiToLanguage( $code)
Allows correcting the language of interlanguage links which, mostly due to legacy reasons,...
Definition: Skin.php:1295
Skin\makeSpecialUrl
static makeSpecialUrl( $name, $urlaction='', $proto=null)
Make a URL for a Special Page using the given query and protocol.
Definition: Skin.php:1204
Skin\getCategoryLinks
getCategoryLinks()
Definition: Skin.php:607
Html\expandAttributes
static expandAttributes(array $attribs)
Given an associative array of element attributes, generate a string to stick after the element name i...
Definition: Html.php:479
Skin\$action
string $action
cached action for cheap lookup
Definition: Skin.php:78
Skin\wrapHTML
wrapHTML( $title, $html)
Wrap the body text with language information and identifiable element.
Definition: Skin.php:2595
Skin\makeSpecialUrlSubpage
static makeSpecialUrlSubpage( $name, $subpage, $urlaction='')
Definition: Skin.php:1219
Skin\getJsConfigVars
getJsConfigVars()
Returns array of config variables that should be added only to this skin for use in JavaScript.
Definition: Skin.php:2526
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:131
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:672
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:710
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:595
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:1933
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:2290
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:734
Linker\tooltipAndAccesskeyAttribs
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2220
Skin\afterContentHook
afterContentHook()
This runs a hook to allow extensions placing their stuff after content and article metadata (e....
Definition: Skin.php:708
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:153
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:53
SpecialPage\getSafeTitleFor
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
Definition: SpecialPage.php:160
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:319
Skin\getCachedNotice
getCachedNotice( $name)
Get a cached notice.
Definition: Skin.php:1851
UserrightsPage
Special page to allow managing user group membership.
Definition: SpecialUserrights.php:37
Skin\prepareUndeleteLink
prepareUndeleteLink()
Prepare undelete link for output in page.
Definition: Skin.php:2568
Article\newFromWikiPage
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:195
MWException
MediaWiki exception.
Definition: MWException.php:29
Skin\getIndicatorsData
getIndicatorsData( $indicators)
Return an array of indicator data.
Definition: Skin.php:2046
Skin\buildSidebar
buildSidebar()
Build an array that represents the sidebar(s), the navigation bar among them.
Definition: Skin.php:1586
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
Skin\prepareUserLanguageAttributes
prepareUserLanguageAttributes()
Prepare user language attribute links.
Definition: Skin.php:2557
Skin\normalizeKey
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:209
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:1052
Skin\setRelevantUser
setRelevantUser(?UserIdentity $u)
Definition: Skin.php:503
$wgFallbackSkin
$wgFallbackSkin
Fallback skin used when the skin defined by $wgDefaultSkin can't be found.
Definition: DefaultSettings.php:3843
Skin\$stylename
string $stylename
Stylesheets set to use.
Definition: Skin.php:72
Skin\getCategories
getCategories()
Definition: Skin.php:673
Skin\getSearchPageTitle
getSearchPageTitle()
Definition: Skin.php:2624
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:1641
Skin\getLanguages
getLanguages()
Generates array of language links for the current page.
Definition: Skin.php:1308
$title
$title
Definition: testCompression.php:38
Skin\preloadExistence
preloadExistence()
Preload the existence of three commonly-requested pages in a single query.
Definition: Skin.php:434
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:1025
$wgDefaultSkin
$wgDefaultSkin
Default skin, for new users and anonymous visitors.
Definition: DefaultSettings.php:3836
$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:1991
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:578
Skin\$mRelevantTitle
$mRelevantTitle
Definition: Skin.php:61
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:495
$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:1170
Skin\getPageClasses
getPageClasses( $title)
TODO: document.
Definition: Skin.php:560
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
Skin\makeSearchInput
makeSearchInput( $attrs=[])
Definition: Skin.php:2386
Skin\getDefaultModules
getDefaultModules()
Defines the ResourceLoader modules that should be added to the skin It is recommended that skins wish...
Definition: Skin.php:352
Skin\getOptions
getOptions()
Returns skin options Recommended to use SkinFactory::getSkinOptions instead.
Definition: Skin.php:2650
BaseTemplate\getPoweredByHTML
static getPoweredByHTML(Config $config)
Definition: BaseTemplate.php:68
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
$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:757
Skin\getSearchInputAttributes
getSearchInputAttributes( $attrs=[])
Definition: Skin.php:2365
OutputPage\getHTMLTitle
getHTMLTitle()
Return the "HTML title", i.e.
Definition: OutputPage.php:960
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:1414
Skin\getAfterPortlet
getAfterPortlet(string $name)
Allows extensions to hook into known portlets and add stuff to them.
Definition: Skin.php:2448
Skin\__construct
__construct( $options=null)
Definition: Skin.php:268
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:2041
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:45
Skin\getUserLanguageAttributes
getUserLanguageAttributes()
Get user language attribute links array.
Definition: Skin.php:2535
Skin\logoText
logoText( $align='')
Definition: Skin.php:990
$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:1744
Skin\getVersion
static getVersion()
Get the current major version of Skin.
Definition: Skin.php:87
Title
Represents a title within MediaWiki.
Definition: Title.php:47
Skin\getSectionsData
getSectionsData()
Enriches section data with has-subsections and is-last-subsection properties so that the table of con...
Definition: Skin.php:107
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:890
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:255
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:213
SpecialPage\setContext
setContext( $context)
Sets the context this SpecialPage is executed in.
Definition: SpecialPage.php:778
Skin\getRelevantUser
getRelevantUser()
Return the "relevant" user.
Definition: Skin.php:516
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:776
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:1262
CreditsAction
Definition: CreditsAction.php:32
Skin\makeMainPageUrl
static makeMainPageUrl( $urlaction='')
Definition: Skin.php:1187
Skin\getFooterLinks
getFooterLinks()
Get template representation of the footer containing site footer links as well as standard footer lin...
Definition: Skin.php:2481
$t
$t
Definition: testCompression.php:74
Skin\getCopyrightIcon
getCopyrightIcon()
Definition: Skin.php:934
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:235
Skin\getSkinName
getSkinName()
Definition: Skin.php:290
NS_FILE
const NS_FILE
Definition: Defines.php:70
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:44
Skin\getPortletLinkOptions
static getPortletLinkOptions(ResourceLoaderContext $context)
Returns skin options for portlet links, used by addPortletLink.
Definition: Skin.php:2672
Skin\initPage
initPage(OutputPage $out)
Definition: Skin.php:315
Skin\setRelevantTitle
setRelevantTitle( $t)
Definition: Skin.php:481
Skin\isResponsive
isResponsive()
Indicates if this skin is responsive.
Definition: Skin.php:303
Skin\makeSearchButton
makeSearchButton( $mode, $attrs=[])
Definition: Skin.php:2397
Skin\addToSidebarPlain
addToSidebarPlain(&$bar, $text)
Add content from plain text.
Definition: Skin.php:1652
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
Skin\getTemplateData
getTemplateData()
Subclasses may extend this method to add additional template data.
Definition: Skin.php:143
Skin\$mRelevantUser
UserIdentity null false $mRelevantUser
Definition: Skin.php:66
SpecialPage\newSearchPage
static newSearchPage(User $user)
Get the users preferred search page.
Definition: SpecialPage.php:103
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1230
BaseTemplate\getCopyrightIconHTML
static getCopyrightIconHTML(Config $config, Skin $skin)
Definition: BaseTemplate.php:41
Skin\addClassToClassList
addClassToClassList( $class, string $newClass)
Adds a class to the existing class value, supporting it as a string or array.
Definition: Skin.php:2349
Skin\setSearchPageTitle
setSearchPageTitle(Title $title)
Definition: Skin.php:2633
Skin\makeFooterIcon
makeFooterIcon( $icon, $withImage='withImage')
Renders a $wgFooterIcons icon according to the method's arguments.
Definition: Skin.php:1012
$type
$type
Definition: testCompression.php:52