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 
99  public static function normalizeKey( $key ) {
101 
102  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
103  $skinNames = $skinFactory->getInstalledSkins();
104 
105  // Make keys lowercase for case-insensitive matching.
106  $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
107  $key = strtolower( $key );
108  $defaultSkin = strtolower( $wgDefaultSkin );
109  $fallbackSkin = strtolower( $wgFallbackSkin );
110 
111  if ( $key == '' || $key == 'default' ) {
112  // Don't return the default immediately;
113  // in a misconfiguration we need to fall back.
114  $key = $defaultSkin;
115  }
116 
117  if ( isset( $skinNames[$key] ) ) {
118  return $key;
119  }
120 
121  // Older versions of the software used a numeric setting
122  // in the user preferences.
123  $fallback = [
124  0 => $defaultSkin,
125  2 => 'cologneblue'
126  ];
127 
128  if ( isset( $fallback[$key] ) ) {
129  $key = $fallback[$key];
130  }
131 
132  if ( isset( $skinNames[$key] ) ) {
133  return $key;
134  } elseif ( isset( $skinNames[$defaultSkin] ) ) {
135  return $defaultSkin;
136  } else {
137  return $fallbackSkin;
138  }
139  }
140 
154  public function __construct( $options = null ) {
155  if ( is_string( $options ) ) {
156  $this->skinname = $options;
157  } elseif ( $options ) {
158  $name = $options['name'] ?? null;
159 
160  if ( !$name ) {
161  throw new SkinException( 'Skin name must be specified' );
162  }
163 
164  if ( isset( $options['link'] ) ) {
165  $this->defaultLinkOptions = $options['link'];
166  }
167  $this->options = $options;
168  $this->skinname = $name;
169  }
170  }
171 
175  public function getSkinName() {
176  return $this->skinname;
177  }
178 
188  public function isResponsive() {
189  $isSkinResponsiveCapable = $this->options['responsive'] ?? false;
190  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
191 
192  return $isSkinResponsiveCapable &&
193  $userOptionsLookup->getBoolOption( $this->getUser(), 'skin-responsive' );
194  }
195 
200  public function initPage( OutputPage $out ) {
201  $skinMetaTags = $this->getConfig()->get( 'SkinMetaTags' );
202  $this->preloadExistence();
203 
204  if ( $this->isResponsive() ) {
205  $out->addMeta(
206  'viewport',
207  'width=device-width, initial-scale=1.0, ' .
208  'user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0'
209  );
210  }
211 
212  $tags = [
213  'og:title' => $out->getHTMLTitle(),
214  'twitter:card' => 'summary_large_image',
215  'og:type' => 'website',
216  ];
217 
218  // Support sharing on platforms such as Facebook and Twitter
219  foreach ( $tags as $key => $value ) {
220  if ( in_array( $key, $skinMetaTags ) ) {
221  $out->addMeta( $key, $value );
222  }
223  }
224  }
225 
237  public function getDefaultModules() {
238  $out = $this->getOutput();
239  $user = $this->getUser();
240 
241  // Modules declared in the $modules literal are loaded
242  // for ALL users, on ALL pages, in ALL skins.
243  // Keep this list as small as possible!
244  $modules = [
245  // The 'styles' key sets render-blocking style modules
246  // Unlike other keys in $modules, this is an associative array
247  // where each key is its own group pointing to a list of modules
248  'styles' => [
249  'skin' => $this->options['styles'] ?? [],
250  'core' => [],
251  'content' => [],
252  'syndicate' => [],
253  ],
254  'core' => [
255  'site',
256  'mediawiki.page.ready',
257  ],
258  // modules that enhance the content in some way
259  'content' => [],
260  // modules relating to search functionality
261  'search' => [],
262  // Skins can register their own scripts
263  'skin' => $this->options['scripts'] ?? [],
264  // modules relating to functionality relating to watching an article
265  'watch' => [],
266  // modules which relate to the current users preferences
267  'user' => [],
268  // modules relating to RSS/Atom Feeds
269  'syndicate' => [],
270  ];
271 
272  // Preload jquery.tablesorter for mediawiki.page.ready
273  if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
274  $modules['content'][] = 'jquery.tablesorter';
275  $modules['styles']['content'][] = 'jquery.tablesorter.styles';
276  }
277 
278  // Preload jquery.makeCollapsible for mediawiki.page.ready
279  if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
280  $modules['content'][] = 'jquery.makeCollapsible';
281  $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
282  }
283 
284  // Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
285  // on every page is deprecated. Express a dependency instead.
286  if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
287  $modules['styles']['content'][] = 'mediawiki.ui.button';
288  }
289 
290  if ( $out->isTOCEnabled() ) {
291  $modules['content'][] = 'mediawiki.toc';
292  }
293 
294  $authority = $this->getAuthority();
295  if ( $authority->getUser()->isRegistered()
296  && $authority->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
297  && $this->getRelevantTitle()->canExist()
298  ) {
299  $modules['watch'][] = 'mediawiki.page.watch.ajax';
300  }
301 
302  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
303  if ( $userOptionsLookup->getBoolOption( $user, 'editsectiononrightclick' )
304  || ( $out->isArticle() && $userOptionsLookup->getOption( $user, 'editondblclick' ) )
305  ) {
306  $modules['user'][] = 'mediawiki.misc-authed-pref';
307  }
308 
309  if ( $out->isSyndicated() ) {
310  $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
311  }
312 
313  return $modules;
314  }
315 
319  protected function preloadExistence() {
320  $titles = [];
321 
322  // User/talk link
323  $user = $this->getUser();
324  if ( $user->isRegistered() ) {
325  $titles[] = $user->getUserPage();
326  $titles[] = $user->getTalkPage();
327  }
328 
329  // Check, if the page can hold some kind of content, otherwise do nothing
330  $title = $this->getRelevantTitle();
331  if ( $title->canExist() && $title->canHaveTalkPage() ) {
332  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
333  if ( $title->isTalkPage() ) {
334  $titles[] = $namespaceInfo->getSubjectPage( $title );
335  } else {
336  $titles[] = $namespaceInfo->getTalkPage( $title );
337  }
338  }
339 
340  // Footer links (used by SkinTemplate::prepareQuickTemplate)
341  if ( $this->getConfig()->get( 'FooterLinkCacheExpiry' ) <= 0 ) {
342  $titles = array_merge(
343  $titles,
344  array_filter( [
345  $this->footerLinkTitle( 'privacy', 'privacypage' ),
346  $this->footerLinkTitle( 'aboutsite', 'aboutpage' ),
347  $this->footerLinkTitle( 'disclaimers', 'disclaimerpage' ),
348  ] )
349  );
350  }
351 
352  $this->getHookRunner()->onSkinPreloadExistence( $titles, $this );
353 
354  if ( $titles ) {
355  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
356  $lb = $linkBatchFactory->newLinkBatch( $titles );
357  $lb->setCaller( __METHOD__ );
358  $lb->execute();
359  }
360  }
361 
366  public function setRelevantTitle( $t ) {
367  $this->mRelevantTitle = $t;
368  }
369 
380  public function getRelevantTitle() {
381  return $this->mRelevantTitle ?? $this->getTitle();
382  }
383 
388  public function setRelevantUser( ?UserIdentity $u ) {
389  $this->mRelevantUser = $u;
390  }
391 
401  public function getRelevantUser(): ?UserIdentity {
402  if ( $this->mRelevantUser === false ) {
403  $this->mRelevantUser = null; // false indicates we never attempted to load it.
404  $title = $this->getRelevantTitle();
405  if ( $title->hasSubjectNamespace( NS_USER ) ) {
406  $services = MediaWikiServices::getInstance();
407  $rootUser = $title->getRootText();
408  $userNameUtils = $services->getUserNameUtils();
409  if ( $userNameUtils->isIP( $rootUser ) ) {
410  $this->mRelevantUser = UserIdentityValue::newAnonymous( $rootUser );
411  } else {
412  $user = $services->getUserIdentityLookup()->getUserIdentityByName( $rootUser );
413  $this->mRelevantUser = $user && $user->isRegistered() ? $user : null;
414  }
415  }
416  }
417 
418  // The relevant user should only be set if it exists. However, if it exists but is hidden,
419  // and the viewer cannot see hidden users, this exposes the fact that the user exists;
420  // pretend like the user does not exist in such cases, by setting it to null. T120883
421  if ( $this->mRelevantUser && $this->mRelevantUser->isRegistered() ) {
422  $userBlock = MediaWikiServices::getInstance()
423  ->getBlockManager()
424  ->getUserBlock( $this->mRelevantUser, null, true );
425  if ( $userBlock && $userBlock->getHideName() &&
426  !$this->getAuthority()->isAllowed( 'hideuser' )
427  ) {
428  $this->mRelevantUser = null;
429  }
430  }
431 
432  return $this->mRelevantUser;
433  }
434 
438  abstract public function outputPage();
439 
445  public function getPageClasses( $title ) {
446  $numeric = 'ns-' . $title->getNamespace();
447 
448  if ( $title->isSpecialPage() ) {
449  $type = 'ns-special';
450  // T25315: provide a class based on the canonical special page name without subpages
451  list( $canonicalName ) = MediaWikiServices::getInstance()->getSpecialPageFactory()->
452  resolveAlias( $title->getDBkey() );
453  if ( $canonicalName ) {
454  $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
455  } else {
456  $type .= ' mw-invalidspecialpage';
457  }
458  } else {
459  if ( $title->isTalkPage() ) {
460  $type = 'ns-talk';
461  } else {
462  $type = 'ns-subject';
463  }
464  // T208315: add HTML class when the user can edit the page
465  if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
466  $type .= ' mw-editable';
467  }
468  }
469 
470  $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() );
471  $root = Sanitizer::escapeClass( 'rootpage-' . $title->getRootTitle()->getPrefixedText() );
472 
473  return "$numeric $type $name $root";
474  }
475 
480  public function getHtmlElementAttributes() {
481  $lang = $this->getLanguage();
482  return [
483  'lang' => $lang->getHtmlCode(),
484  'dir' => $lang->getDir(),
485  'class' => 'client-nojs',
486  ];
487  }
488 
492  public function getCategoryLinks() {
493  $out = $this->getOutput();
494  $allCats = $out->getCategoryLinks();
495  $title = $this->getTitle();
496  $services = MediaWikiServices::getInstance();
497  $linkRenderer = $services->getLinkRenderer();
498 
499  if ( $allCats === [] ) {
500  return '';
501  }
502 
503  $embed = "<li>";
504  $pop = "</li>";
505 
506  $s = '';
507  $colon = $this->msg( 'colon-separator' )->escaped();
508 
509  if ( !empty( $allCats['normal'] ) ) {
510  $t = $embed . implode( $pop . $embed, $allCats['normal'] ) . $pop;
511 
512  $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) );
513  $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
514  $pageCategoriesLinkTitle = Title::newFromText( $linkPage );
515  if ( $pageCategoriesLinkTitle ) {
516  $link = $linkRenderer->makeLink( $pageCategoriesLinkTitle, $msg->text() );
517  } else {
518  $link = $msg->escaped();
519  }
520  $s .= Html::rawElement(
521  'div',
522  [ 'id' => 'mw-normal-catlinks', 'class' => 'mw-normal-catlinks' ],
523  $link . $colon . Html::rawElement( 'ul', [], $t )
524  );
525  }
526 
527  # Hidden categories
528  if ( isset( $allCats['hidden'] ) ) {
529  $userOptionsLookup = $services->getUserOptionsLookup();
530 
531  if ( $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' ) ) {
532  $class = ' mw-hidden-cats-user-shown';
533  } elseif ( $title->inNamespace( NS_CATEGORY ) ) {
534  $class = ' mw-hidden-cats-ns-shown';
535  } else {
536  $class = ' mw-hidden-cats-hidden';
537  }
538 
539  $s .= Html::rawElement(
540  'div',
541  [ 'id' => 'mw-hidden-catlinks', 'class' => "mw-hidden-catlinks$class" ],
542  $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
543  $colon .
545  'ul',
546  [],
547  $embed . implode( $pop . $embed, $allCats['hidden'] ) . $pop
548  )
549  );
550  }
551 
552  # optional 'dmoz-like' category browser. Will be shown under the list
553  # of categories an article belong to
554  if ( $this->getConfig()->get( 'UseCategoryBrowser' ) ) {
555  $s .= '<br /><hr />';
556 
557  # get a big array of the parents tree
558  $parenttree = $title->getParentCategoryTree();
559  # Skin object passed by reference cause it can not be
560  # accessed under the method subfunction drawCategoryBrowser
561  $tempout = explode( "\n", $this->drawCategoryBrowser( $parenttree ) );
562  # Clean out bogus first entry and sort them
563  unset( $tempout[0] );
564  asort( $tempout );
565  # Output one per line
566  $s .= implode( "<br />\n", $tempout );
567  }
568 
569  return $s;
570  }
571 
577  protected function drawCategoryBrowser( $tree ) {
578  $return = '';
579  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
580 
581  foreach ( $tree as $element => $parent ) {
582  if ( empty( $parent ) ) {
583  # element start a new list
584  $return .= "\n";
585  } else {
586  # grab the others elements
587  $return .= $this->drawCategoryBrowser( $parent ) . ' &gt; ';
588  }
589 
590  # add our current element to the list
591  $eltitle = Title::newFromText( $element );
592  $return .= $linkRenderer->makeLink( $eltitle, $eltitle->getText() );
593  }
594 
595  return $return;
596  }
597 
601  public function getCategories() {
602  $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
603  $showHiddenCats = $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' );
604 
605  $catlinks = $this->getCategoryLinks();
606  // Check what we're showing
607  $allCats = $this->getOutput()->getCategoryLinks();
608  $showHidden = $showHiddenCats || $this->getTitle()->inNamespace( NS_CATEGORY );
609 
610  $classes = [ 'catlinks' ];
611  if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
612  $classes[] = 'catlinks-allhidden';
613  }
614 
615  return Html::rawElement(
616  'div',
617  [ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
618  $catlinks
619  );
620  }
621 
636  protected function afterContentHook() {
637  $data = '';
638 
639  if ( $this->getHookRunner()->onSkinAfterContent( $data, $this ) ) {
640  // adding just some spaces shouldn't toggle the output
641  // of the whole <div/>, so we use trim() here
642  if ( trim( $data ) != '' ) {
643  // Doing this here instead of in the skins to
644  // ensure that the div has the same ID in all
645  // skins
646  $data = "<div id='mw-data-after-content'>\n" .
647  "\t$data\n" .
648  "</div>\n";
649  }
650  } else {
651  wfDebug( "Hook SkinAfterContent changed output processing." );
652  }
653 
654  return $data;
655  }
656 
662  public function bottomScripts() {
663  // TODO and the suckage continues. This function is really just a wrapper around
664  // OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
665  // up at some point
666  $chunks = [ $this->getOutput()->getBottomScripts() ];
667 
668  // Keep the hook appendage separate to preserve WrappedString objects.
669  // This enables BaseTemplate::getTrail() to merge them where possible.
670  $extraHtml = '';
671  $this->getHookRunner()->onSkinAfterBottomScripts( $this, $extraHtml );
672  if ( $extraHtml !== '' ) {
673  $chunks[] = $extraHtml;
674  }
675  return WrappedString::join( "\n", $chunks );
676  }
677 
685  public function printSource() {
686  $title = $this->getTitle();
687  $oldid = $this->getOutput()->getRevisionId();
688  if ( $oldid ) {
689  $canonicalUrl = $title->getCanonicalURL( 'oldid=' . $oldid );
690  $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
691  } else {
692  // oldid not available for non existing pages
693  $url = htmlspecialchars( wfExpandIRI( $title->getCanonicalURL() ) );
694  }
695 
696  return $this->msg( 'retrievedfrom' )
697  ->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
698  ->parse();
699  }
700 
704  public function getUndeleteLink() {
705  $action = $this->getRequest()->getRawVal( 'action', 'view' );
706  $title = $this->getTitle();
707  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
708 
709  if ( ( !$title->exists() || $action == 'history' ) &&
710  $this->getAuthority()->probablyCan( 'deletedhistory', $title )
711  ) {
712  $n = $title->getDeletedEditsCount();
713 
714  if ( $n ) {
715  if ( $this->getAuthority()->probablyCan( 'undelete', $title ) ) {
716  $msg = 'thisisdeleted';
717  } else {
718  $msg = 'viewdeleted';
719  }
720 
721  $subtitle = $this->msg( $msg )->rawParams(
722  $linkRenderer->makeKnownLink(
723  SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ),
724  $this->msg( 'restorelink' )->numParams( $n )->text() )
725  )->escaped();
726 
727  $links = [];
728  // Add link to page logs, unless we're on the history page (which
729  // already has one)
730  if ( $action !== 'history' ) {
731  $links[] = $linkRenderer->makeKnownLink(
732  SpecialPage::getTitleFor( 'Log' ),
733  $this->msg( 'viewpagelogs-lowercase' )->text(),
734  [],
735  [ 'page' => $title->getPrefixedText() ]
736  );
737  }
738 
739  // Allow extensions to add more links
740  $this->getHookRunner()->onUndeletePageToolLinks(
741  $this->getContext(), $linkRenderer, $links );
742 
743  if ( $links ) {
744  $subtitle .= ''
745  . $this->msg( 'word-separator' )->escaped()
746  . $this->msg( 'parentheses' )
747  ->rawParams( $this->getLanguage()->pipeList( $links ) )
748  ->escaped();
749  }
750 
751  return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
752  }
753  }
754 
755  return '';
756  }
757 
761  private function subPageSubtitleInternal() {
762  $services = MediaWikiServices::getInstance();
763  $linkRenderer = $services->getLinkRenderer();
764  $out = $this->getOutput();
765  $title = $out->getTitle();
766  $subpages = '';
767 
768  if ( !$this->getHookRunner()->onSkinSubPageSubtitle( $subpages, $this, $out ) ) {
769  return $subpages;
770  }
771 
772  $hasSubpages = $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() );
773  if ( !$out->isArticle() || !$hasSubpages ) {
774  return $subpages;
775  }
776 
777  $ptext = $title->getPrefixedText();
778  if ( strpos( $ptext, '/' ) !== false ) {
779  $links = explode( '/', $ptext );
780  array_pop( $links );
781  $count = 0;
782  $growingLink = '';
783  $display = '';
784  $lang = $this->getLanguage();
785 
786  foreach ( $links as $link ) {
787  $growingLink .= $link;
788  $display .= $link;
789  $linkObj = Title::newFromText( $growingLink );
790 
791  if ( $linkObj && $linkObj->isKnown() ) {
792  $getlink = $linkRenderer->makeKnownLink( $linkObj, $display );
793 
794  $count++;
795 
796  if ( $count > 1 ) {
797  $subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
798  } else {
799  $subpages .= '&lt; ';
800  }
801 
802  $subpages .= $getlink;
803  $display = '';
804  } else {
805  $display .= '/';
806  }
807  $growingLink .= '/';
808  }
809  }
810 
811  return $subpages;
812  }
813 
818  public function getCopyright( $type = 'detect' ) {
819  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
820  if ( $type == 'detect' ) {
821  if ( !$this->getOutput()->isRevisionCurrent()
822  && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
823  ) {
824  $type = 'history';
825  } else {
826  $type = 'normal';
827  }
828  }
829 
830  if ( $type == 'history' ) {
831  $msg = 'history_copyright';
832  } else {
833  $msg = 'copyright';
834  }
835 
836  $config = $this->getConfig();
837 
838  if ( $config->get( 'RightsPage' ) ) {
839  $title = Title::newFromText( $config->get( 'RightsPage' ) );
840  $link = $linkRenderer->makeKnownLink(
841  $title, new HtmlArmor( $config->get( 'RightsText' ) ?: $title->getText() )
842  );
843  } elseif ( $config->get( 'RightsUrl' ) ) {
844  $link = Linker::makeExternalLink( $config->get( 'RightsUrl' ), $config->get( 'RightsText' ) );
845  } elseif ( $config->get( 'RightsText' ) ) {
846  $link = $config->get( 'RightsText' );
847  } else {
848  # Give up now
849  return '';
850  }
851 
852  // Allow for site and per-namespace customization of copyright notice.
853  $this->getHookRunner()->onSkinCopyrightFooter( $this->getTitle(), $type, $msg, $link );
854 
855  return $this->msg( $msg )->rawParams( $link )->text();
856  }
857 
862  protected function getCopyrightIcon() {
863  wfDeprecated( __METHOD__, '1.37' );
864  return BaseTemplate::getCopyrightIconHTML( $this->getConfig(), $this );
865  }
866 
872  protected function getPoweredBy() {
873  wfDeprecated( __METHOD__, '1.37' );
874  $text = BaseTemplate::getPoweredByHTML( $this->getConfig() );
875  $this->getHookRunner()->onSkinGetPoweredBy( $text, $this );
876  return $text;
877  }
878 
884  protected function lastModified() {
885  $timestamp = $this->getOutput()->getRevisionTimestamp();
886  $user = $this->getUser();
887  $language = $this->getLanguage();
888 
889  # No cached timestamp, load it from the database
890  if ( $timestamp === null ) {
891  $revId = $this->getOutput()->getRevisionId();
892  if ( $revId !== null ) {
893  $timestamp = MediaWikiServices::getInstance()
894  ->getRevisionLookup()
895  ->getTimestampFromId( $revId );
896  }
897  }
898 
899  if ( $timestamp ) {
900  $d = $language->userDate( $timestamp, $user );
901  $t = $language->userTime( $timestamp, $user );
902  $s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->parse();
903  } else {
904  $s = '';
905  }
906 
907  if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getLaggedReplicaMode() ) {
908  $s .= ' <strong>' . $this->msg( 'laggedreplicamode' )->parse() . '</strong>';
909  }
910 
911  return $s;
912  }
913 
918  public function logoText( $align = '' ) {
919  if ( $align != '' ) {
920  $a = " style='float: {$align};'";
921  } else {
922  $a = '';
923  }
924 
925  $mp = $this->msg( 'mainpage' )->escaped();
926  $url = htmlspecialchars( Title::newMainPage()->getLocalURL() );
927 
928  $logourl = ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() )[ '1x' ];
929  return "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
930  }
931 
940  public function makeFooterIcon( $icon, $withImage = 'withImage' ) {
941  if ( is_string( $icon ) ) {
942  $html = $icon;
943  } else { // Assuming array
944  $url = $icon['url'] ?? null;
945  unset( $icon['url'] );
946  if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
947  // Lazy-load footer icons, since they're not part of the printed view.
948  $icon['loading'] = 'lazy';
949  // do this the lazy way, just pass icon data as an attribute array
950  $html = Html::element( 'img', $icon );
951  } else {
952  $html = htmlspecialchars( $icon['alt'] ?? '' );
953  }
954  if ( $url ) {
955  $html = Html::rawElement( 'a',
956  [ 'href' => $url, 'target' => $this->getConfig()->get( 'ExternalLinkTarget' ) ],
957  $html );
958  }
959  }
960  return $html;
961  }
962 
980  public function footerLink( $desc, $page ) {
981  $title = $this->footerLinkTitle( $desc, $page );
982 
983  if ( !$title ) {
984  return '';
985  }
986 
987  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
988  return $linkRenderer->makeKnownLink(
989  $title,
990  $this->msg( $desc )->text()
991  );
992  }
993 
999  private function footerLinkTitle( $desc, $page ) {
1000  // If the link description has been disabled in the default language,
1001  if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
1002  // then it is disabled, for all languages.
1003  return null;
1004  }
1005  // Otherwise, we display the link for the user, described in their
1006  // language (which may or may not be the same as the default language),
1007  // but we make the link target be the one site-wide page.
1008  $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
1009 
1010  return $title ?: null;
1011  }
1012 
1019  public function getSiteFooterLinks() {
1020  $callback = function () {
1021  return [
1022  'privacy' => $this->footerLink( 'privacy', 'privacypage' ),
1023  'about' => $this->footerLink( 'aboutsite', 'aboutpage' ),
1024  'disclaimer' => $this->footerLink( 'disclaimers', 'disclaimerpage' )
1025  ];
1026  };
1027 
1028  $services = MediaWikiServices::getInstance();
1029  $msgCache = $services->getMessageCache();
1030  $wanCache = $services->getMainWANObjectCache();
1031  $config = $this->getConfig();
1032 
1033  return ( $config->get( 'FooterLinkCacheExpiry' ) > 0 )
1034  ? $wanCache->getWithSetCallback(
1035  $wanCache->makeKey( 'footer-links' ),
1036  $config->get( 'FooterLinkCacheExpiry' ),
1037  $callback,
1038  [
1039  'checkKeys' => [
1040  // Unless there is both no exact $code override nor an i18n definition
1041  // in the software, the only MediaWiki page to check is for $code.
1042  $msgCache->getCheckKey( $this->getLanguage()->getCode() )
1043  ],
1044  'lockTSE' => 30
1045  ]
1046  )
1047  : $callback();
1048  }
1049 
1057  public function editUrlOptions() {
1058  $options = [ 'action' => 'edit' ];
1059  $out = $this->getOutput();
1060 
1061  if ( !$out->isRevisionCurrent() ) {
1062  $options['oldid'] = intval( $out->getRevisionId() );
1063  }
1064 
1065  return $options;
1066  }
1067 
1072  public function showEmailUser( $id ) {
1073  if ( $id instanceof UserIdentity ) {
1074  $targetUser = User::newFromIdentity( $id );
1075  } else {
1076  $targetUser = User::newFromId( $id );
1077  }
1078 
1079  # The sending user must have a confirmed email address and the receiving
1080  # user must accept emails from the sender.
1081  return $this->getUser()->canSendEmail()
1082  && SpecialEmailUser::validateTarget( $targetUser, $this->getUser() ) === '';
1083  }
1084 
1098  public function getSkinStylePath( $name ) {
1099  wfDeprecated( __METHOD__, '1.36' );
1100 
1101  if ( $this->stylename === null ) {
1102  $class = static::class;
1103  throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
1104  }
1105 
1106  return $this->getConfig()->get( 'StylePath' ) . "/{$this->stylename}/$name";
1107  }
1108 
1109  /* these are used extensively in SkinTemplate, but also some other places */
1110 
1115  public static function makeMainPageUrl( $urlaction = '' ) {
1117 
1118  return $title->getLinkURL( $urlaction );
1119  }
1120 
1132  public static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1134  if ( $proto === null ) {
1135  return $title->getLocalURL( $urlaction );
1136  } else {
1137  return $title->getFullURL( $urlaction, false, $proto );
1138  }
1139  }
1140 
1147  public static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1148  $title = SpecialPage::getSafeTitleFor( $name, $subpage );
1149  return $title->getLocalURL( $urlaction );
1150  }
1151 
1158  public static function makeInternalOrExternalUrl( $name ) {
1159  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
1160  return $name;
1161  } else {
1162  $title = Title::newFromText( $name );
1163  self::checkTitle( $title, $name );
1164  return $title->getLocalURL();
1165  }
1166  }
1167 
1174  protected static function makeUrlDetails( $name, $urlaction = '' ) {
1175  $title = Title::newFromText( $name );
1176  self::checkTitle( $title, $name );
1177 
1178  return [
1179  'href' => $title->getLocalURL( $urlaction ),
1180  'exists' => $title->isKnown(),
1181  ];
1182  }
1183 
1190  protected static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1191  $title = Title::newFromText( $name );
1192  self::checkTitle( $title, $name );
1193 
1194  return [
1195  'href' => $title->getLocalURL( $urlaction ),
1196  'exists' => true
1197  ];
1198  }
1199 
1206  public static function checkTitle( &$title, $name ) {
1207  if ( !is_object( $title ) ) {
1208  $title = Title::newFromText( $name );
1209  if ( !is_object( $title ) ) {
1210  $title = Title::newFromText( '--error: link target missing--' );
1211  }
1212  }
1213  }
1214 
1223  public function mapInterwikiToLanguage( $code ) {
1224  $map = $this->getConfig()->get( 'InterlanguageLinkCodeMap' );
1225  return $map[ $code ] ?? $code;
1226  }
1227 
1236  public function getLanguages() {
1237  if ( $this->getConfig()->get( 'HideInterlanguageLinks' ) ) {
1238  return [];
1239  }
1240  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1241 
1242  $userLang = $this->getLanguage();
1243  $languageLinks = [];
1244  $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1245 
1246  foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
1247  $class = 'interlanguage-link interwiki-' . explode( ':', $languageLinkText, 2 )[0];
1248 
1249  $languageLinkTitle = Title::newFromText( $languageLinkText );
1250  if ( !$languageLinkTitle ) {
1251  continue;
1252  }
1253 
1254  $ilInterwikiCode = $this->mapInterwikiToLanguage( $languageLinkTitle->getInterwiki() );
1255 
1256  $ilLangName = $langNameUtils->getLanguageName( $ilInterwikiCode );
1257 
1258  if ( strval( $ilLangName ) === '' ) {
1259  $ilDisplayTextMsg = $this->msg( "interlanguage-link-$ilInterwikiCode" );
1260  if ( !$ilDisplayTextMsg->isDisabled() ) {
1261  // Use custom MW message for the display text
1262  $ilLangName = $ilDisplayTextMsg->text();
1263  } else {
1264  // Last resort: fallback to the language link target
1265  $ilLangName = $languageLinkText;
1266  }
1267  } else {
1268  // Use the language autonym as display text
1269  $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
1270  }
1271 
1272  // CLDR extension or similar is required to localize the language name;
1273  // otherwise we'll end up with the autonym again.
1274  $ilLangLocalName = $langNameUtils->getLanguageName(
1275  $ilInterwikiCode,
1276  $userLang->getCode()
1277  );
1278 
1279  $languageLinkTitleText = $languageLinkTitle->getText();
1280  if ( $ilLangLocalName === '' ) {
1281  $ilFriendlySiteName = $this->msg( "interlanguage-link-sitename-$ilInterwikiCode" );
1282  if ( !$ilFriendlySiteName->isDisabled() ) {
1283  if ( $languageLinkTitleText === '' ) {
1284  $ilTitle = $this->msg(
1285  'interlanguage-link-title-nonlangonly',
1286  $ilFriendlySiteName->text()
1287  )->text();
1288  } else {
1289  $ilTitle = $this->msg(
1290  'interlanguage-link-title-nonlang',
1291  $languageLinkTitleText,
1292  $ilFriendlySiteName->text()
1293  )->text();
1294  }
1295  } else {
1296  // we have nothing friendly to put in the title, so fall back to
1297  // displaying the interlanguage link itself in the title text
1298  // (similar to what is done in page content)
1299  $ilTitle = $languageLinkTitle->getInterwiki() .
1300  ":$languageLinkTitleText";
1301  }
1302  } elseif ( $languageLinkTitleText === '' ) {
1303  $ilTitle = $this->msg(
1304  'interlanguage-link-title-langonly',
1305  $ilLangLocalName
1306  )->text();
1307  } else {
1308  $ilTitle = $this->msg(
1309  'interlanguage-link-title',
1310  $languageLinkTitleText,
1311  $ilLangLocalName
1312  )->text();
1313  }
1314 
1315  $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
1316  $languageLink = [
1317  'href' => $languageLinkTitle->getFullURL(),
1318  'text' => $ilLangName,
1319  'title' => $ilTitle,
1320  'class' => $class,
1321  'link-class' => 'interlanguage-link-target',
1322  'lang' => $ilInterwikiCodeBCP47,
1323  'hreflang' => $ilInterwikiCodeBCP47,
1324  ];
1325  $hookContainer->run(
1326  'SkinTemplateGetLanguageLink',
1327  [ &$languageLink, $languageLinkTitle, $this->getTitle(), $this->getOutput() ],
1328  []
1329  );
1330  $languageLinks[] = $languageLink;
1331  }
1332 
1333  return $languageLinks;
1334  }
1335 
1342  protected function buildNavUrls() {
1343  $out = $this->getOutput();
1344  $title = $this->getTitle();
1345  $thispage = $title->getPrefixedDBkey();
1346  $uploadNavigationUrl = $this->getConfig()->get( 'UploadNavigationUrl' );
1347 
1348  $nav_urls = [];
1349  $nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1350  if ( $uploadNavigationUrl ) {
1351  $nav_urls['upload'] = [ 'href' => $uploadNavigationUrl ];
1352  } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
1353  $nav_urls['upload'] = [ 'href' => self::makeSpecialUrl( 'Upload' ) ];
1354  } else {
1355  $nav_urls['upload'] = false;
1356  }
1357  $nav_urls['specialpages'] = [ 'href' => self::makeSpecialUrl( 'Specialpages' ) ];
1358 
1359  $nav_urls['print'] = false;
1360  $nav_urls['permalink'] = false;
1361  $nav_urls['info'] = false;
1362  $nav_urls['whatlinkshere'] = false;
1363  $nav_urls['recentchangeslinked'] = false;
1364  $nav_urls['contributions'] = false;
1365  $nav_urls['log'] = false;
1366  $nav_urls['blockip'] = false;
1367  $nav_urls['mute'] = false;
1368  $nav_urls['emailuser'] = false;
1369  $nav_urls['userrights'] = false;
1370 
1371  // A print stylesheet is attached to all pages, but nobody ever
1372  // figures that out. :) Add a link...
1373  if ( !$out->isPrintable() && ( $out->isArticle() || $title->isSpecialPage() ) ) {
1374  $nav_urls['print'] = [
1375  'text' => $this->msg( 'printableversion' )->text(),
1376  'href' => 'javascript:print();'
1377  ];
1378  }
1379 
1380  if ( $out->isArticle() ) {
1381  // Also add a "permalink" while we're at it
1382  $revid = $out->getRevisionId();
1383  if ( $revid ) {
1384  $nav_urls['permalink'] = [
1385  'text' => $this->msg( 'permalink' )->text(),
1386  'href' => $title->getLocalURL( "oldid=$revid" )
1387  ];
1388  }
1389  }
1390 
1391  if ( $out->isArticleRelated() ) {
1392  $nav_urls['whatlinkshere'] = [
1393  'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $thispage )->getLocalURL()
1394  ];
1395 
1396  $nav_urls['info'] = [
1397  'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1398  'href' => $title->getLocalURL( "action=info" )
1399  ];
1400 
1401  if ( $title->exists() || $title->inNamespace( NS_CATEGORY ) ) {
1402  $nav_urls['recentchangeslinked'] = [
1403  'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $thispage )->getLocalURL()
1404  ];
1405  }
1406  }
1407 
1408  $user = $this->getRelevantUser();
1409 
1410  if ( $user ) {
1411  $rootUser = $user->getName();
1412 
1413  $nav_urls['contributions'] = [
1414  'text' => $this->msg( 'tool-link-contributions', $rootUser )->text(),
1415  'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1416  'tooltip-params' => [ $rootUser ],
1417  ];
1418 
1419  $nav_urls['log'] = [
1420  'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
1421  ];
1422 
1423  if ( $this->getAuthority()->isAllowed( 'block' ) ) {
1424  $nav_urls['blockip'] = [
1425  'text' => $this->msg( 'blockip', $rootUser )->text(),
1426  'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser )
1427  ];
1428  }
1429 
1430  if ( $this->showEmailUser( $user ) ) {
1431  $nav_urls['emailuser'] = [
1432  'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1433  'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1434  'tooltip-params' => [ $rootUser ],
1435  ];
1436  }
1437 
1438  if ( $user->isRegistered() ) {
1439  if ( $this->getUser()->isRegistered() && $this->getConfig()->get( 'EnableSpecialMute' ) ) {
1440  $nav_urls['mute'] = [
1441  'text' => $this->msg( 'mute-preferences' )->text(),
1442  'href' => self::makeSpecialUrlSubpage( 'Mute', $rootUser )
1443  ];
1444  }
1445 
1446  $sur = new UserrightsPage;
1447  $sur->setContext( $this->getContext() );
1448  $canChange = $sur->userCanChangeRights( $user );
1449  $nav_urls['userrights'] = [
1450  'text' => $this->msg(
1451  $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly',
1452  $rootUser
1453  )->text(),
1454  'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
1455  ];
1456  }
1457  }
1458 
1459  return $nav_urls;
1460  }
1461 
1467  final protected function buildFeedUrls() {
1468  $feeds = [];
1469  $out = $this->getOutput();
1470  if ( $out->isSyndicated() ) {
1471  foreach ( $out->getSyndicationLinks() as $format => $link ) {
1472  $feeds[$format] = [
1473  // Messages: feed-atom, feed-rss
1474  'text' => $this->msg( "feed-$format" )->text(),
1475  'href' => $link
1476  ];
1477  }
1478  }
1479  return $feeds;
1480  }
1481 
1506  public function buildSidebar() {
1507  $services = MediaWikiServices::getInstance();
1508  $callback = function ( $old = null, &$ttl = null ) {
1509  $bar = [];
1510  $this->addToSidebar( $bar, 'sidebar' );
1511  $this->getHookRunner()->onSkinBuildSidebar( $this, $bar );
1512  $msgCache = MediaWikiServices::getInstance()->getMessageCache();
1513  if ( $msgCache->isDisabled() ) {
1514  $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
1515  }
1516 
1517  return $bar;
1518  };
1519 
1520  $msgCache = $services->getMessageCache();
1521  $wanCache = $services->getMainWANObjectCache();
1522  $config = $this->getConfig();
1523  $languageCode = $this->getLanguage()->getCode();
1524 
1525  $sidebar = $config->get( 'EnableSidebarCache' )
1526  ? $wanCache->getWithSetCallback(
1527  $wanCache->makeKey( 'sidebar', $languageCode ),
1528  $config->get( 'SidebarCacheExpiry' ),
1529  $callback,
1530  [
1531  'checkKeys' => [
1532  // Unless there is both no exact $code override nor an i18n definition
1533  // in the software, the only MediaWiki page to check is for $code.
1534  $msgCache->getCheckKey( $languageCode )
1535  ],
1536  'lockTSE' => 30
1537  ]
1538  )
1539  : $callback();
1540 
1541  $sidebar['TOOLBOX'] = $this->makeToolbox(
1542  $this->buildNavUrls(),
1543  $this->buildFeedUrls()
1544  );
1545  $sidebar['LANGUAGES'] = $this->getLanguages();
1546  // Apply post-processing to the cached value
1547  $this->getHookRunner()->onSidebarBeforeOutput( $this, $sidebar );
1548 
1549  return $sidebar;
1550  }
1551 
1561  public function addToSidebar( &$bar, $message ) {
1562  $this->addToSidebarPlain( $bar, $this->msg( $message )->inContentLanguage()->plain() );
1563  }
1564 
1572  public function addToSidebarPlain( &$bar, $text ) {
1573  $lines = explode( "\n", $text );
1574 
1575  $heading = '';
1576  $config = $this->getConfig();
1577  $messageTitle = $config->get( 'EnableSidebarCache' )
1578  ? Title::newMainPage() : $this->getTitle();
1579  $messageCache = MediaWikiServices::getInstance()->getMessageCache();
1580 
1581  foreach ( $lines as $line ) {
1582  if ( strpos( $line, '*' ) !== 0 ) {
1583  continue;
1584  }
1585  $line = rtrim( $line, "\r" ); // for Windows compat
1586 
1587  if ( strpos( $line, '**' ) !== 0 ) {
1588  $heading = trim( $line, '* ' );
1589  if ( !array_key_exists( $heading, $bar ) ) {
1590  $bar[$heading] = [];
1591  }
1592  } else {
1593  $line = trim( $line, '* ' );
1594 
1595  if ( strpos( $line, '|' ) !== false ) { // sanity check
1596  $line = $messageCache->transform( $line, false, null, $messageTitle );
1597  $line = array_map( 'trim', explode( '|', $line, 2 ) );
1598  if ( count( $line ) !== 2 ) {
1599  // Second sanity check, could be hit by people doing
1600  // funky stuff with parserfuncs... (T35321)
1601  continue;
1602  }
1603 
1604  $extraAttribs = [];
1605 
1606  $msgLink = $this->msg( $line[0] )->page( $messageTitle )->inContentLanguage();
1607  if ( $msgLink->exists() ) {
1608  $link = $msgLink->text();
1609  if ( $link == '-' ) {
1610  continue;
1611  }
1612  } else {
1613  $link = $line[0];
1614  }
1615  $msgText = $this->msg( $line[1] )->page( $messageTitle );
1616  if ( $msgText->exists() ) {
1617  $text = $msgText->text();
1618  } else {
1619  $text = $line[1];
1620  }
1621 
1622  if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
1623  $href = $link;
1624 
1625  // Parser::getExternalLinkAttribs won't work here because of the Namespace things
1626  if ( $config->get( 'NoFollowLinks' ) &&
1627  !wfMatchesDomainList( $href, $config->get( 'NoFollowDomainExceptions' ) )
1628  ) {
1629  $extraAttribs['rel'] = 'nofollow';
1630  }
1631 
1632  if ( $config->get( 'ExternalLinkTarget' ) ) {
1633  $extraAttribs['target'] = $config->get( 'ExternalLinkTarget' );
1634  }
1635  } else {
1636  $title = Title::newFromText( $link );
1637 
1638  if ( $title ) {
1639  $title = $title->fixSpecialName();
1640  $href = $title->getLinkURL();
1641  } else {
1642  $href = 'INVALID-TITLE';
1643  }
1644  }
1645 
1646  $bar[$heading][] = array_merge( [
1647  'text' => $text,
1648  'href' => $href,
1649  'id' => Sanitizer::escapeIdForAttribute( 'n-' . strtr( $line[1], ' ', '-' ) ),
1650  'active' => false,
1651  ], $extraAttribs );
1652  }
1653  }
1654  }
1655 
1656  return $bar;
1657  }
1658 
1664  public function getNewtalks() {
1665  $newMessagesAlert = '';
1666  $user = $this->getUser();
1667  $services = MediaWikiServices::getInstance();
1668  $linkRenderer = $services->getLinkRenderer();
1669  $userHasNewMessages = $services->getTalkPageNotificationManager()
1670  ->userHasNewMessages( $user );
1671  $timestamp = $services->getTalkPageNotificationManager()
1672  ->getLatestSeenMessageTimestamp( $user );
1673  $newtalks = !$userHasNewMessages ? [] : [
1674  [
1675  // TODO: Deprecate adding wiki and link to array and redesign GetNewMessagesAlert hook
1676  'wiki' => WikiMap::getCurrentWikiId(),
1677  'link' => $user->getTalkPage()->getLocalURL(),
1678  'rev' => $timestamp ? $services->getRevisionLookup()
1679  ->getRevisionByTimestamp( $user->getTalkPage(), $timestamp ) : null
1680  ]
1681  ];
1682  $out = $this->getOutput();
1683 
1684  // Allow extensions to disable or modify the new messages alert
1685  if ( !$this->getHookRunner()->onGetNewMessagesAlert(
1686  $newMessagesAlert, $newtalks, $user, $out )
1687  ) {
1688  return '';
1689  }
1690  if ( $newMessagesAlert ) {
1691  return $newMessagesAlert;
1692  }
1693 
1694  if ( $newtalks !== [] ) {
1695  $uTalkTitle = $user->getTalkPage();
1696  $lastSeenRev = $newtalks[0]['rev'];
1697  $numAuthors = 0;
1698  if ( $lastSeenRev !== null ) {
1699  $plural = true; // Default if we have a last seen revision: if unknown, use plural
1700  $revStore = $services->getRevisionStore();
1701  $latestRev = $revStore->getRevisionByTitle(
1702  $uTalkTitle,
1703  0,
1704  RevisionLookup::READ_NORMAL
1705  );
1706  if ( $latestRev !== null ) {
1707  // Singular if only 1 unseen revision, plural if several unseen revisions.
1708  $plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1709  $numAuthors = $revStore->countAuthorsBetween(
1710  $uTalkTitle->getArticleID(),
1711  $lastSeenRev,
1712  $latestRev,
1713  null,
1714  10,
1715  RevisionStore::INCLUDE_NEW
1716  );
1717  }
1718  } else {
1719  // Singular if no revision -> diff link will show latest change only in any case
1720  $plural = false;
1721  }
1722  $plural = $plural ? 999 : 1;
1723  // 999 signifies "more than one revision". We don't know how many, and even if we did,
1724  // the number of revisions or authors is not necessarily the same as the number of
1725  // "messages".
1726  $newMessagesLink = $linkRenderer->makeKnownLink(
1727  $uTalkTitle,
1728  $this->msg( 'newmessageslinkplural' )->params( $plural )->text(),
1729  [],
1730  $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
1731  );
1732 
1733  $newMessagesDiffLink = $linkRenderer->makeKnownLink(
1734  $uTalkTitle,
1735  $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->text(),
1736  [],
1737  $lastSeenRev !== null
1738  ? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1739  : [ 'diff' => 'cur' ]
1740  );
1741 
1742  if ( $numAuthors >= 1 && $numAuthors <= 10 ) {
1743  $newMessagesAlert = $this->msg(
1744  'youhavenewmessagesfromusers',
1745  $newMessagesLink,
1746  $newMessagesDiffLink
1747  )->numParams( $numAuthors, $plural );
1748  } else {
1749  // $numAuthors === 11 signifies "11 or more" ("more than 10")
1750  $newMessagesAlert = $this->msg(
1751  $numAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
1752  $newMessagesLink,
1753  $newMessagesDiffLink
1754  )->numParams( $plural );
1755  }
1756  $newMessagesAlert = $newMessagesAlert->text();
1757  // Disable CDN cache
1758  $out->setCdnMaxage( 0 );
1759  }
1760 
1761  return $newMessagesAlert;
1762  }
1763 
1771  private function getCachedNotice( $name ) {
1772  $config = $this->getConfig();
1773 
1774  if ( $name === 'default' ) {
1775  // special case
1776  $notice = $config->get( 'SiteNotice' );
1777  if ( empty( $notice ) ) {
1778  return false;
1779  }
1780  } else {
1781  $msg = $this->msg( $name )->inContentLanguage();
1782  if ( $msg->isBlank() ) {
1783  return '';
1784  } elseif ( $msg->isDisabled() ) {
1785  return false;
1786  }
1787  $notice = $msg->plain();
1788  }
1789 
1790  $services = MediaWikiServices::getInstance();
1791  $cache = $services->getMainWANObjectCache();
1792  $parsed = $cache->getWithSetCallback(
1793  // Use the extra hash appender to let eg SSL variants separately cache
1794  // Key is verified with md5 hash of unparsed wikitext
1795  $cache->makeKey( $name, $config->get( 'RenderHashAppend' ), md5( $notice ) ),
1796  // TTL in seconds
1797  600,
1798  function () use ( $notice ) {
1799  return $this->getOutput()->parseAsInterface( $notice );
1800  }
1801  );
1802 
1803  $contLang = $services->getContentLanguage();
1804  return Html::rawElement(
1805  'div',
1806  [
1807  'id' => 'localNotice',
1808  'lang' => $contLang->getHtmlCode(),
1809  'dir' => $contLang->getDir()
1810  ],
1811  $parsed
1812  );
1813  }
1814 
1818  public function getSiteNotice() {
1819  $siteNotice = '';
1820 
1821  if ( $this->getHookRunner()->onSiteNoticeBefore( $siteNotice, $this ) ) {
1822  if ( $this->getUser()->isRegistered() ) {
1823  $siteNotice = $this->getCachedNotice( 'sitenotice' );
1824  } else {
1825  $anonNotice = $this->getCachedNotice( 'anonnotice' );
1826  if ( $anonNotice === false ) {
1827  $siteNotice = $this->getCachedNotice( 'sitenotice' );
1828  } else {
1829  $siteNotice = $anonNotice;
1830  }
1831  }
1832  if ( $siteNotice === false ) {
1833  $siteNotice = $this->getCachedNotice( 'default' ) ?: '';
1834  }
1835  }
1836 
1837  $this->getHookRunner()->onSiteNoticeAfter( $siteNotice, $this );
1838  return $siteNotice;
1839  }
1840 
1853  public function doEditSectionLink( Title $nt, $section, $tooltip, Language $lang ) {
1854  // HTML generated here should probably have userlangattributes
1855  // added to it for LTR text on RTL pages
1856 
1857  $attribs = [];
1858  if ( $tooltip !== null ) {
1859  $attribs['title'] = $this->msg( 'editsectionhint' )->rawParams( $tooltip )
1860  ->inLanguage( $lang )->text();
1861  }
1862 
1863  $links = [
1864  'editsection' => [
1865  'text' => $this->msg( 'editsection' )->inLanguage( $lang )->text(),
1866  'targetTitle' => $nt,
1867  'attribs' => $attribs,
1868  'query' => [ 'action' => 'edit', 'section' => $section ]
1869  ]
1870  ];
1871 
1872  $this->getHookRunner()->onSkinEditSectionLinks( $this, $nt, $section, $tooltip, $links, $lang );
1873 
1874  $result = Html::openElement( 'span', [ 'class' => 'mw-editsection' ] );
1875  $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], '[' );
1876 
1877  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1878  $linksHtml = [];
1879  foreach ( $links as $k => $linkDetails ) {
1880  $linksHtml[] = $linkRenderer->makeKnownLink(
1881  $linkDetails['targetTitle'],
1882  $linkDetails['text'],
1883  $linkDetails['attribs'],
1884  $linkDetails['query']
1885  );
1886  }
1887 
1888  $result .= implode(
1890  'span',
1891  [ 'class' => 'mw-editsection-divider' ],
1892  $this->msg( 'pipe-separator' )->inLanguage( $lang )->escaped()
1893  ),
1894  $linksHtml
1895  );
1896 
1897  $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], ']' );
1898  $result .= Html::closeElement( 'span' );
1899  return $result;
1900  }
1901 
1911  public function makeToolbox( $navUrls, $feedUrls ) {
1912  $toolbox = [];
1913  if ( $navUrls['whatlinkshere'] ?? null ) {
1914  $toolbox['whatlinkshere'] = $navUrls['whatlinkshere'];
1915  $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
1916  }
1917  if ( $navUrls['recentchangeslinked'] ?? null ) {
1918  $toolbox['recentchangeslinked'] = $navUrls['recentchangeslinked'];
1919  $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
1920  $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
1921  $toolbox['recentchangeslinked']['rel'] = 'nofollow';
1922  }
1923  if ( $feedUrls ) {
1924  $toolbox['feeds']['id'] = 'feedlinks';
1925  $toolbox['feeds']['links'] = [];
1926  foreach ( $feedUrls as $key => $feed ) {
1927  $toolbox['feeds']['links'][$key] = $feed;
1928  $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
1929  $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
1930  $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
1931  $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
1932  }
1933  }
1934  foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
1935  'userrights', 'upload', 'specialpages' ] as $special
1936  ) {
1937  if ( $navUrls[$special] ?? null ) {
1938  $toolbox[$special] = $navUrls[$special];
1939  $toolbox[$special]['id'] = "t-$special";
1940  }
1941  }
1942  if ( $navUrls['print'] ?? null ) {
1943  $toolbox['print'] = $navUrls['print'];
1944  $toolbox['print']['id'] = 't-print';
1945  $toolbox['print']['rel'] = 'alternate';
1946  $toolbox['print']['msg'] = 'printableversion';
1947  }
1948  if ( $navUrls['permalink'] ?? null ) {
1949  $toolbox['permalink'] = $navUrls['permalink'];
1950  $toolbox['permalink']['id'] = 't-permalink';
1951  }
1952  if ( $navUrls['info'] ?? null ) {
1953  $toolbox['info'] = $navUrls['info'];
1954  $toolbox['info']['id'] = 't-info';
1955  }
1956 
1957  return $toolbox;
1958  }
1959 
1966  protected function getIndicatorsData( $indicators ) {
1967  $indicatorData = [];
1968  foreach ( $indicators as $id => $content ) {
1969  $indicatorData[] = [
1970  'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
1971  'class' => 'mw-indicator',
1972  'html' => $content,
1973  ];
1974  }
1975  return $indicatorData;
1976  }
1977 
1992  final public function getPersonalToolsForMakeListItem( $urls, $applyClassesToListItems = false ) {
1993  $personal_tools = [];
1994  foreach ( $urls as $key => $plink ) {
1995  # The class on a personal_urls item is meant to go on the <a> instead
1996  # of the <li> so we have to use a single item "links" array instead
1997  # of using most of the personal_url's keys directly.
1998  $ptool = [
1999  'links' => [
2000  [ 'single-id' => "pt-$key" ],
2001  ],
2002  'id' => "pt-$key",
2003  ];
2004  if ( $applyClassesToListItems && isset( $plink['class'] ) ) {
2005  $ptool['class'] = $plink['class'];
2006  }
2007  if ( isset( $plink['active'] ) ) {
2008  $ptool['active'] = $plink['active'];
2009  }
2010  // Set class for the link to link-class, when defined.
2011  // This allows newer notifications content navigation to retain their classes
2012  // when merged back into the personal tools.
2013  // Doing this here allows the loop below to overwrite the class if defined directly.
2014  if ( isset( $plink['link-class'] ) ) {
2015  $ptool['links'][0]['class'] = $plink['link-class'];
2016  }
2017  $props = [
2018  'href',
2019  'text',
2020  'dir',
2021  'data',
2022  'exists',
2023  'data-mw'
2024  ];
2025  if ( !$applyClassesToListItems ) {
2026  $props[] = 'class';
2027  }
2028  foreach ( $props as $k ) {
2029  if ( isset( $plink[$k] ) ) {
2030  $ptool['links'][0][$k] = $plink[$k];
2031  }
2032  }
2033  $personal_tools[$key] = $ptool;
2034  }
2035  return $personal_tools;
2036  }
2037 
2097  final public function makeLink( $key, $item, $linkOptions = [] ) {
2098  $options = $linkOptions + $this->defaultLinkOptions;
2099  $text = $item['text'] ?? $this->msg( $item['msg'] ?? $key )->text();
2100 
2101  $html = htmlspecialchars( $text );
2102 
2103  if ( isset( $options['text-wrapper'] ) ) {
2104  $wrapper = $options['text-wrapper'];
2105  if ( isset( $wrapper['tag'] ) ) {
2106  $wrapper = [ $wrapper ];
2107  }
2108  while ( count( $wrapper ) > 0 ) {
2109  $element = array_pop( $wrapper );
2110  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
2111  $html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
2112  }
2113  }
2114 
2115  if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
2116  $attrs = $item;
2117  foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
2118  'tooltip-params', 'exists', 'link-html' ] as $k ) {
2119  unset( $attrs[$k] );
2120  }
2121 
2122  if ( isset( $attrs['data'] ) ) {
2123  foreach ( $attrs['data'] as $key => $value ) {
2124  $attrs[ 'data-' . $key ] = $value;
2125  }
2126  unset( $attrs[ 'data' ] );
2127  }
2128 
2129  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2130  $item['single-id'] = $item['id'];
2131  }
2132 
2133  $tooltipParams = [];
2134  if ( isset( $item['tooltip-params'] ) ) {
2135  $tooltipParams = $item['tooltip-params'];
2136  }
2137 
2138  if ( isset( $item['single-id'] ) ) {
2139  $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
2140 
2141  if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
2142  $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
2143  if ( $title !== false ) {
2144  $attrs['title'] = $title;
2145  }
2146  } else {
2148  $item['single-id'],
2149  $tooltipParams,
2150  $tooltipOption
2151  );
2152  if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
2153  $attrs['title'] = $tip['title'];
2154  }
2155  if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
2156  $attrs['accesskey'] = $tip['accesskey'];
2157  }
2158  }
2159  }
2160  if ( isset( $options['link-class'] ) ) {
2161  $attrs['class'] = $this->addClassToClassList( $attrs['class'] ?? [], $options['link-class'] );
2162  }
2163 
2164  if ( isset( $item['link-html'] ) ) {
2165  $html = $item['link-html'] . ' ' . $html;
2166  }
2167 
2168  $html = Html::rawElement( isset( $attrs['href'] )
2169  ? 'a'
2170  : $options['link-fallback'], $attrs, $html );
2171  }
2172 
2173  return $html;
2174  }
2175 
2210  final public function makeListItem( $key, $item, $options = [] ) {
2211  // In case this is still set from SkinTemplate, we don't want it to appear in
2212  // the HTML output (normally removed in SkinTemplate::buildContentActionUrls())
2213  unset( $item['redundant'] );
2214 
2215  if ( isset( $item['links'] ) ) {
2216  $links = [];
2217  foreach ( $item['links'] as $link ) {
2218  // Note: links will have identical label unless 'msg' is set on $link
2219  $links[] = $this->makeLink( $key, $link, $options );
2220  }
2221  $html = implode( ' ', $links );
2222  } else {
2223  $link = $item;
2224  // These keys are used by makeListItem and shouldn't be passed on to the link
2225  foreach ( [ 'id', 'class', 'active', 'tag', 'itemtitle' ] as $k ) {
2226  unset( $link[$k] );
2227  }
2228  if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
2229  // The id goes on the <li> not on the <a> for single links
2230  // but makeSidebarLink still needs to know what id to use when
2231  // generating tooltips and accesskeys.
2232  $link['single-id'] = $item['id'];
2233  }
2234  if ( isset( $link['link-class'] ) ) {
2235  // link-class should be set on the <a> itself,
2236  // so pass it in as 'class'
2237  $link['class'] = $link['link-class'];
2238  unset( $link['link-class'] );
2239  }
2240  $html = $this->makeLink( $key, $link, $options );
2241  }
2242 
2243  $attrs = [];
2244  foreach ( [ 'id', 'class' ] as $attr ) {
2245  if ( isset( $item[$attr] ) ) {
2246  $attrs[$attr] = $item[$attr];
2247  }
2248  }
2249  $attrs['class'] = $this->addClassToClassList( $attrs['class'] ?? [], 'mw-list-item' );
2250 
2251  if ( isset( $item['active'] ) && $item['active'] ) {
2252  // In the future, this should accept an array of classes, not a string
2253  $attrs['class'] = $this->addClassToClassList( $attrs['class'], 'active' );
2254  }
2255  if ( isset( $item['itemtitle'] ) ) {
2256  $attrs['title'] = $item['itemtitle'];
2257  }
2258  return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
2259  }
2260 
2269  private function addClassToClassList( $class, string $newClass ) {
2270  if ( is_array( $class ) ) {
2271  $class[] = $newClass;
2272  } else {
2273  $class .= ' ' . $newClass;
2274  $class = trim( $class );
2275  }
2276  return $class;
2277  }
2278 
2285  protected function getSearchInputAttributes( $attrs = [] ) {
2286  $autoCapHint = $this->getConfig()->get( 'CapitalLinks' );
2287  $realAttrs = [
2288  'type' => 'search',
2289  'name' => 'search',
2290  'placeholder' => $this->msg( 'searchsuggest-search' )->text(),
2291  'aria-label' => $this->msg( 'searchsuggest-search' )->text(),
2292  // T251664: Disable autocapitalization of input
2293  // method when using fully case-sensitive titles.
2294  'autocapitalize' => $autoCapHint ? 'sentences' : 'none',
2295  ];
2296 
2297  return array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs );
2298  }
2299 
2306  final public function makeSearchInput( $attrs = [] ) {
2307  return Html::element( 'input', $this->getSearchInputAttributes( $attrs ) );
2308  }
2309 
2317  final public function makeSearchButton( $mode, $attrs = [] ) {
2318  switch ( $mode ) {
2319  case 'go':
2320  case 'fulltext':
2321  $realAttrs = [
2322  'type' => 'submit',
2323  'name' => $mode,
2324  'value' => $this->msg( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
2325  ];
2326  $realAttrs = array_merge(
2327  $realAttrs,
2328  Linker::tooltipAndAccesskeyAttribs( "search-$mode" ),
2329  $attrs
2330  );
2331  return Html::element( 'input', $realAttrs );
2332  case 'image':
2333  $buttonAttrs = [
2334  'type' => 'submit',
2335  'name' => 'button',
2336  ];
2337  $buttonAttrs = array_merge(
2338  $buttonAttrs,
2339  Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ),
2340  $attrs
2341  );
2342  unset( $buttonAttrs['src'] );
2343  unset( $buttonAttrs['alt'] );
2344  unset( $buttonAttrs['width'] );
2345  unset( $buttonAttrs['height'] );
2346  $imgAttrs = [
2347  'src' => $attrs['src'],
2348  'alt' => $attrs['alt'] ?? $this->msg( 'searchbutton' )->text(),
2349  'width' => $attrs['width'] ?? null,
2350  'height' => $attrs['height'] ?? null,
2351  ];
2352  return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) );
2353  default:
2354  throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' );
2355  }
2356  }
2357 
2368  public function getAfterPortlet( string $name ): string {
2369  $html = '';
2370 
2371  $this->getHookRunner()->onSkinAfterPortlet( $this, $name, $html );
2372 
2373  return $html;
2374  }
2375 
2381  final public function prepareSubtitle() {
2382  $out = $this->getOutput();
2383  $subpagestr = $this->subPageSubtitleInternal();
2384  if ( $subpagestr !== '' ) {
2385  $subpagestr = Html::rawElement( 'span', [ 'class' => 'subpages' ], $subpagestr );
2386  }
2387  return $subpagestr . $out->getSubtitle();
2388  }
2389 
2401  protected function getFooterLinks(): array {
2402  $out = $this->getOutput();
2403  $title = $out->getTitle();
2404  $titleExists = $title->exists();
2405  $config = $this->getConfig();
2406  $maxCredits = $config->get( 'MaxCredits' );
2407  $showCreditsIfMax = $config->get( 'ShowCreditsIfMax' );
2408  $useCredits = $titleExists
2409  && $out->isArticle()
2410  && $out->isRevisionCurrent()
2411  && $maxCredits !== 0;
2412 
2414  if ( $useCredits ) {
2415  $article = Article::newFromWikiPage( $this->getWikiPage(), $this );
2416  $action = Action::factory( 'credits', $article, $this );
2417  }
2418 
2419  '@phan-var CreditsAction $action';
2420  $data = [
2421  'info' => [
2422  'lastmod' => !$useCredits ? $this->lastModified() : null,
2423  'numberofwatchingusers' => null,
2424  'credits' => $useCredits ?
2425  $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
2426  'copyright' => $titleExists &&
2427  $out->showsCopyright() ? $this->getCopyright() : null,
2428  ],
2429  'places' => $this->getSiteFooterLinks(),
2430  ];
2431  foreach ( $data as $key => $existingItems ) {
2432  $newItems = [];
2433  $this->getHookRunner()->onSkinAddFooterLinks( $this, $key, $newItems );
2434  $data[$key] = $existingItems + $newItems;
2435  }
2436  return $data;
2437  }
2438 
2439  public function getSearchPageTitle(): Title {
2440  return $this->searchPageTitle ?? SpecialPage::getTitleFor( 'Search' );
2441  }
2442 
2443  public function setSearchPageTitle( Title $title ) {
2444  $this->searchPageTitle = $title;
2445  }
2446 
2454  public function getOptions(): array {
2455  return $this->options;
2456  }
2457 
2472  public static function getPortletLinkOptions( ResourceLoaderContext $context ): array {
2473  $skinName = $context->getSkin();
2474  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2475  $options = $skinFactory->getSkinOptions( $skinName );
2476  $portletLinkOptions = $options['link'] ?? [];
2477  // Normalize link options to always have this key
2478  $portletLinkOptions += [ 'text-wrapper' => [] ];
2479  // Normalize text-wrapper to always be an array of arrays
2480  if ( isset( $portletLinkOptions['text-wrapper']['tag'] ) ) {
2481  $portletLinkOptions['text-wrapper'] = [ $portletLinkOptions['text-wrapper'] ];
2482  }
2483  return $portletLinkOptions;
2484  }
2485 }
Skin\prepareSubtitle
prepareSubtitle()
Prepare the subtitle of the page for output in the skin if one has been set.
Definition: Skin.php:2381
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\$context
IContextSource $context
Definition: ContextSource.php:39
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:34
Skin\editUrlOptions
editUrlOptions()
Return URL options for the 'edit page' link.
Definition: Skin.php:1057
Skin\showEmailUser
showEmailUser( $id)
Definition: Skin.php:1072
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:647
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:382
Skin\makeUrlDetails
static makeUrlDetails( $name, $urlaction='')
these return an array with the 'href' and boolean 'exists'
Definition: Skin.php:1174
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
Skin\getSiteNotice
getSiteNotice()
Definition: Skin.php:1818
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:2097
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:1467
Skin\subPageSubtitleInternal
subPageSubtitleInternal()
Definition: Skin.php:761
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:999
IContextSource\getSkin
getSkin()
Skin\getPoweredBy
getPoweredBy()
Gets the powered by MediaWiki icon.
Definition: Skin.php:872
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:199
$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:1992
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
Sanitizer\escapeIdForAttribute
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:812
true
return true
Definition: router.php:90
$fallback
$fallback
Definition: MessagesAb.php:11
Skin\lastModified
lastModified()
Get the timestamp of the latest revision, formatted in user language.
Definition: Skin.php:884
Skin\getSiteFooterLinks
getSiteFooterLinks()
Gets the link to the wiki's privacy policy, about page, and disclaimer page.
Definition: Skin.php:1019
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:1206
Skin\mapInterwikiToLanguage
mapInterwikiToLanguage( $code)
Allows correcting the language of interlanguage links which, mostly due to legacy reasons,...
Definition: Skin.php:1223
Skin\makeSpecialUrl
static makeSpecialUrl( $name, $urlaction='', $proto=null)
Make a URL for a Special Page using the given query and protocol.
Definition: Skin.php:1132
Skin\getCategoryLinks
getCategoryLinks()
Definition: Skin.php:492
Skin\makeSpecialUrlSubpage
static makeSpecialUrlSubpage( $name, $subpage, $urlaction='')
Definition: Skin.php:1147
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:107
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:973
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:683
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:712
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:480
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:1853
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:2210
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:662
Linker\tooltipAndAccesskeyAttribs
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2181
Skin\afterContentHook
afterContentHook()
This runs a hook to allow extensions placing their stuff after content and article metadata (e....
Definition: Skin.php:636
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
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:316
Skin\getCachedNotice
getCachedNotice( $name)
Get a cached notice.
Definition: Skin.php:1771
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:195
MWException
MediaWiki exception.
Definition: MWException.php:29
Skin\getIndicatorsData
getIndicatorsData( $indicators)
Return an array of indicator data.
Definition: Skin.php:1966
Skin\buildSidebar
buildSidebar()
Build an array that represents the sidebar(s), the navigation bar among them.
Definition: Skin.php:1506
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
Skin\normalizeKey
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:99
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:980
Skin\setRelevantUser
setRelevantUser(?UserIdentity $u)
Definition: Skin.php:388
$wgFallbackSkin
$wgFallbackSkin
Fallback skin used when the skin defined by $wgDefaultSkin can't be found.
Definition: DefaultSettings.php:3835
Skin\$stylename
string $stylename
Stylesheets set to use.
Definition: Skin.php:72
Skin\getCategories
getCategories()
Definition: Skin.php:601
Skin\getSearchPageTitle
getSearchPageTitle()
Definition: Skin.php:2439
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:1561
Skin\drawCategoryBrowser
drawCategoryBrowser( $tree)
Render the array as a series of links.
Definition: Skin.php:577
Skin\getLanguages
getLanguages()
Generates array of language links for the current page.
Definition: Skin.php:1236
$title
$title
Definition: testCompression.php:38
Skin\preloadExistence
preloadExistence()
Preload the existence of three commonly-requested pages in a single query.
Definition: Skin.php:319
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:1011
$wgDefaultSkin
$wgDefaultSkin
Default skin, for new users and anonymous visitors.
Definition: DefaultSettings.php:3828
$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:1911
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:567
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:380
$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:1098
Skin\getPageClasses
getPageClasses( $title)
TODO: document.
Definition: Skin.php:445
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
Skin\makeSearchInput
makeSearchInput( $attrs=[])
Definition: Skin.php:2306
Skin\getDefaultModules
getDefaultModules()
Defines the ResourceLoader modules that should be added to the skin It is recommended that skins wish...
Definition: Skin.php:237
Skin\getOptions
getOptions()
Returns skin options Recommended to use SkinFactory::getSkinOptions instead.
Definition: Skin.php:2454
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:685
Skin\getSearchInputAttributes
getSearchInputAttributes( $attrs=[])
Definition: Skin.php:2285
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:1342
Skin\getAfterPortlet
getAfterPortlet(string $name)
Allows extensions to hook into known portlets and add stuff to them.
Definition: Skin.php:2368
Skin\__construct
__construct( $options=null)
Definition: Skin.php:154
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:2002
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:45
Skin\logoText
logoText( $align='')
Definition: Skin.php:918
$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:1664
Skin\getVersion
static getVersion()
Get the current major version of Skin.
Definition: Skin.php:86
Title
Represents a title within MediaWiki.
Definition: Title.php:47
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:818
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:252
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:754
Skin\getRelevantUser
getRelevantUser()
Return the "relevant" user.
Definition: Skin.php:401
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:704
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:1190
CreditsAction
Definition: CreditsAction.php:32
Skin\makeMainPageUrl
static makeMainPageUrl( $urlaction='')
Definition: Skin.php:1115
Skin\getFooterLinks
getFooterLinks()
Get template representation of the footer containing site footer links as well as standard footer lin...
Definition: Skin.php:2401
$t
$t
Definition: testCompression.php:74
Skin\getCopyrightIcon
getCopyrightIcon()
Definition: Skin.php:862
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:175
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:2472
Skin\initPage
initPage(OutputPage $out)
Definition: Skin.php:200
Skin\setRelevantTitle
setRelevantTitle( $t)
Definition: Skin.php:366
Skin\isResponsive
isResponsive()
Indicates if this skin is responsive.
Definition: Skin.php:188
Skin\makeSearchButton
makeSearchButton( $mode, $attrs=[])
Definition: Skin.php:2317
Skin\addToSidebarPlain
addToSidebarPlain(&$bar, $text)
Add content from plain text.
Definition: Skin.php:1572
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:1158
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:2269
Skin\setSearchPageTitle
setSearchPageTitle(Title $title)
Definition: Skin.php:2443
Skin\makeFooterIcon
makeFooterIcon( $icon, $withImage='withImage')
Renders a $wgFooterIcons icon according to the method's arguments.
Definition: Skin.php:940
$type
$type
Definition: testCompression.php:52