MediaWiki  master
SkinTemplate.php
Go to the documentation of this file.
1 <?php
25 
34 class SkinTemplate extends Skin {
39  public $template;
40 
41  public $thispage;
42  public $titletxt;
43  public $userpage;
44  public $thisquery;
45  // TODO: Rename this to $isRegistered (but that's a breaking change)
46  public $loggedin;
47  public $username;
49 
58  protected function setupTemplate( $classname ) {
59  return new $classname( $this->getConfig() );
60  }
61 
65  protected function setupTemplateForOutput() {
66  $this->setupTemplateContext();
67  $template = $this->options['template'] ?? $this->template;
68  if ( !$template ) {
69  throw new RuntimeException(
70  'SkinTemplate skins must define a `template` either as a public'
71  . ' property of by passing in a`template` option to the constructor.'
72  );
73  }
74  $tpl = $this->setupTemplate( $template );
75  return $tpl;
76  }
77 
87  final protected function setupTemplateContext() {
88  $request = $this->getRequest();
89  $user = $this->getUser();
90  $title = $this->getTitle();
91  $this->thispage = $title->getPrefixedDBkey();
92  $this->titletxt = $title->getPrefixedText();
93  $this->userpage = $user->getUserPage()->getPrefixedText();
94  $query = [];
95  if ( !$request->wasPosted() ) {
96  $query = $request->getValues();
97  unset( $query['title'] );
98  unset( $query['returnto'] );
99  unset( $query['returntoquery'] );
100  }
101  $this->thisquery = wfArrayToCgi( $query );
102  $this->loggedin = $user->isRegistered();
103  $this->username = $user->getName();
104 
105  if ( $this->loggedin ) {
106  $this->userpageUrlDetails = self::makeUrlDetails( $this->userpage );
107  } else {
108  # This won't be used in the standard skins, but we define it to preserve the interface
109  # To save time, we check for existence
110  $this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage );
111  }
112  }
113 
122  public function generateHTML() {
123  $tpl = $this->prepareQuickTemplate();
124  // execute template
125  return $tpl->execute();
126  }
127 
132  public function outputPage() {
133  Profiler::instance()->setAllowOutput();
134  $out = $this->getOutput();
135 
136  $this->initPage( $out );
137  $out->addJsConfigVars( $this->getJsConfigVars() );
138 
139  // result may be an error
140  echo $this->generateHTML();
141  }
142 
148  protected function getFooterIcons() {
149  $config = $this->getConfig();
150 
151  $footericons = [];
152  foreach ( $config->get( 'FooterIcons' ) as $footerIconsKey => &$footerIconsBlock ) {
153  if ( count( $footerIconsBlock ) > 0 ) {
154  $footericons[$footerIconsKey] = [];
155  foreach ( $footerIconsBlock as &$footerIcon ) {
156  if ( isset( $footerIcon['src'] ) ) {
157  if ( !isset( $footerIcon['width'] ) ) {
158  $footerIcon['width'] = 88;
159  }
160  if ( !isset( $footerIcon['height'] ) ) {
161  $footerIcon['height'] = 31;
162  }
163  }
164 
165  // Only output icons which have an image.
166  // For historic reasons this mimics the `icononly` option
167  // for BaseTemplate::getFooterIcons.
168  // In some cases the icon may be an empty array.
169  // Filter these out. (See T269776)
170  if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) {
171  $footericons[$footerIconsKey][] = $footerIcon;
172  }
173  }
174 
175  // If no valid icons with images were added, unset the parent array
176  // Should also prevent empty arrays from when no copyright is set.
177  if ( !count( $footericons[$footerIconsKey] ) ) {
178  unset( $footericons[$footerIconsKey] );
179  }
180  }
181  }
182  return $footericons;
183  }
184 
188  public function getTemplateData() {
189  return parent::getTemplateData() + [
190  // Data objects
191  'data-search-box' => $this->buildSearchProps(),
192  'data-logos' => $this->getLogoData(),
193  ] + $this->getPortletsTemplateData() + $this->getFooterTemplateData();
194  }
195 
202  protected function prepareQuickTemplate() {
203  $title = $this->getTitle();
204  $request = $this->getRequest();
205  $out = $this->getOutput();
206  $config = $this->getConfig();
207  $tpl = $this->setupTemplateForOutput();
208 
209  $tpl->set( 'title', $out->getPageTitle() );
210  $tpl->set( 'pagetitle', $out->getHTMLTitle() );
211  $tpl->set( 'displaytitle', $out->mPageLinkTitle );
212 
213  $tpl->set( 'thispage', $this->thispage );
214  $tpl->set( 'titleprefixeddbkey', $this->thispage );
215  $tpl->set( 'titletext', $title->getText() );
216  $tpl->set( 'articleid', $title->getArticleID() );
217 
218  $tpl->set( 'isarticle', $out->isArticle() );
219 
220  $tpl->set( 'subtitle', $this->prepareSubtitle() );
221  $tpl->set( 'undelete', $this->prepareUndeleteLink() );
222 
223  $tpl->set( 'catlinks', $this->getCategories() );
224  $feeds = $this->buildFeedUrls();
225  $tpl->set( 'feeds', count( $feeds ) ? $feeds : false );
226 
227  $tpl->set( 'mimetype', $config->get( 'MimeType' ) );
228  $tpl->set( 'charset', 'UTF-8' );
229  $tpl->set( 'wgScript', $config->get( 'Script' ) );
230  $tpl->set( 'skinname', $this->skinname );
231  $tpl->set( 'skinclass', static::class );
232  $tpl->set( 'skin', $this );
233  $tpl->set( 'stylename', $this->stylename );
234  $tpl->set( 'printable', $out->isPrintable() );
235  $tpl->set( 'handheld', $request->getBool( 'handheld' ) );
236  $tpl->set( 'loggedin', $this->loggedin );
237  $tpl->set( 'notspecialpage', !$title->isSpecialPage() );
238 
239  // Deprecated since 1.36
240  $searchLink = $this->getSearchPageTitle()->getLocalURL();
241  $tpl->set( 'searchaction', $searchLink );
242 
243  $tpl->set( 'searchtitle', $this->getSearchPageTitle()->getPrefixedDBkey() );
244  $tpl->set( 'search', trim( $request->getVal( 'search' ) ) );
245  $tpl->set( 'stylepath', $config->get( 'StylePath' ) );
246  $tpl->set( 'articlepath', $config->get( 'ArticlePath' ) );
247  $tpl->set( 'scriptpath', $config->get( 'ScriptPath' ) );
248  $tpl->set( 'serverurl', $config->get( 'Server' ) );
250  $tpl->set( 'logopath', $logos['1x'] );
251  $tpl->set( 'sitename', $config->get( 'Sitename' ) );
252 
253  $userLang = $this->getLanguage();
254  $userLangCode = $userLang->getHtmlCode();
255  $userLangDir = $userLang->getDir();
256 
257  $tpl->set( 'lang', $userLangCode );
258  $tpl->set( 'dir', $userLangDir );
259  $tpl->set( 'rtl', $userLang->isRTL() );
260 
261  $tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed
262  $tpl->set( 'username', $this->loggedin ? $this->username : null );
263  $tpl->set( 'userpage', $this->userpage );
264  $tpl->set( 'userpageurl', $this->userpageUrlDetails['href'] );
265  $tpl->set( 'userlang', $userLangCode );
266 
267  // Users can have their language set differently than the
268  // content of the wiki. For these users, tell the web browser
269  // that interface elements are in a different language.
270  $tpl->set( 'userlangattributes', $this->prepareUserLanguageAttributes() );
271  $tpl->set( 'specialpageattributes', '' ); # obsolete
272  // Used by VectorBeta to insert HTML before content but after the
273  // heading for the page title. Defaults to empty string.
274  $tpl->set( 'prebodyhtml', '' );
275 
276  $tpl->set( 'newtalk', $this->getNewtalks() );
277  $tpl->set( 'logo', $this->logoText() );
278 
279  $footerData = $this->getFooterLinks();
280  $tpl->set( 'copyright', $footerData['info']['copyright'] ?? false );
281  // No longer used
282  $tpl->set( 'viewcount', false );
283  $tpl->set( 'lastmod', $footerData['info']['lastmod'] ?? false );
284  $tpl->set( 'credits', $footerData['info']['credits'] ?? false );
285  $tpl->set( 'numberofwatchingusers', false );
286 
287  $tpl->set( 'copyrightico', BaseTemplate::getCopyrightIconHTML( $config, $this ) );
288  $poweredBy = BaseTemplate::getPoweredByHTML( $config );
289  // Run deprecated hook.
290  $this->getHookRunner()->onSkinGetPoweredBy( $poweredBy, $this );
291  $tpl->set( 'poweredbyico', $poweredBy );
292 
293  $tpl->set( 'disclaimer', $footerData['places']['disclaimer'] ?? false );
294  $tpl->set( 'privacy', $footerData['places']['privacy'] ?? false );
295  $tpl->set( 'about', $footerData['places']['about'] ?? false );
296 
297  // Flatten for compat with the 'footerlinks' key in QuickTemplate-based skins.
298  $flattenedfooterlinks = [];
299  foreach ( $footerData as $category => $links ) {
300  $flattenedfooterlinks[$category] = array_keys( $links );
301  foreach ( $links as $key => $value ) {
302  // For full support with BaseTemplate we also need to
303  // copy over the keys.
304  $tpl->set( $key, $value );
305  }
306  }
307  $tpl->set( 'footerlinks', $flattenedfooterlinks );
308  $tpl->set( 'footericons', $this->getFooterIcons() );
309 
310  $tpl->set( 'indicators', $out->getIndicators() );
311 
312  $tpl->set( 'sitenotice', $this->getSiteNotice() );
313  $tpl->set( 'printfooter', $this->printSource() );
314  // Wrap the bodyText with #mw-content-text element
315  $tpl->set( 'bodytext', $this->wrapHTML( $title, $out->getHTML() ) );
316 
317  $tpl->set( 'language_urls', $this->getLanguages() ?: false );
318 
319  $content_navigation = $this->buildContentNavigationUrls();
320  # Personal toolbar
321  $tpl->set( 'personal_urls', $this->makeSkinTemplatePersonalUrls( $content_navigation ) );
322  // The user-menu, notifications, and user-interface-preferences are new content navigation entries which aren't
323  // expected to be part of content_navigation or content_actions. Adding them in there breaks skins that do not
324  // expect it.
325  unset(
326  $content_navigation['user-menu'],
327  $content_navigation['notifications'],
328  $content_navigation['user-page'],
329  $content_navigation['user-interface-preferences']
330  );
331  $content_actions = $this->buildContentActionUrls( $content_navigation );
332  $tpl->set( 'content_navigation', $content_navigation );
333  $tpl->set( 'content_actions', $content_actions );
334 
335  $tpl->set( 'sidebar', $this->buildSidebar() );
336  $tpl->set( 'nav_urls', $this->buildNavUrls() );
337 
338  // Do this last in case hooks above add bottom scripts
339  $tpl->set( 'bottomscripts', $this->bottomScripts() );
340 
341  // Set the head scripts near the end, in case the above actions resulted in added scripts
342  $tpl->set( 'headelement', $out->headElement( $this ) );
343 
344  $tpl->set( 'debug', '' );
345  $tpl->set( 'debughtml', MWDebug::getHTMLDebugLog() );
346  $tpl->set( 'reporttime', wfReportTime( $out->getCSP()->getNonce() ) );
347 
348  // Set the bodytext to another key so that skins can just output it on its own
349  // and output printfooter and debughtml separately
350  $tpl->set( 'bodycontent', $tpl->data['bodytext'] );
351 
352  // Append printfooter and debughtml onto bodytext so that skins that
353  // were already using bodytext before they were split out don't suddenly
354  // start not outputting information.
355  $tpl->data['bodytext'] .= Html::rawElement(
356  'div',
357  [ 'class' => 'printfooter' ],
358  "\n{$tpl->data['printfooter']}"
359  ) . "\n";
360  $tpl->data['bodytext'] .= $tpl->data['debughtml'];
361 
362  // allow extensions adding stuff after the page content.
363  // See Skin::afterContentHook() for further documentation.
364  $tpl->set( 'dataAfterContent', $this->afterContentHook() );
365 
366  return $tpl;
367  }
368 
375  public function getPersonalToolsList() {
376  wfDeprecated( __METHOD__, '1.35' );
377  return $this->makePersonalToolsList();
378  }
379 
390  public function makePersonalToolsList( $personalTools = null, $options = [] ) {
391  $this->setupTemplateContext();
392  $html = '';
393 
394  if ( $personalTools === null ) {
395  $personalTools = $this->getPersonalToolsForMakeListItem(
396  $this->buildPersonalUrls()
397  );
398  }
399 
400  foreach ( $personalTools as $key => $item ) {
401  $html .= $this->makeListItem( $key, $item, $options );
402  }
403 
404  return $html;
405  }
406 
414  public function getStructuredPersonalTools() {
415  // buildPersonalUrls requires the template context.
416  $this->setupTemplateContext();
417  return $this->getPersonalToolsForMakeListItem(
418  $this->buildPersonalUrls()
419  );
420  }
421 
429  protected function buildPersonalUrls( bool $includeNotifications = true ) {
430  $title = $this->getTitle();
431  $request = $this->getRequest();
432  $pageurl = $title->getLocalURL();
433  $services = MediaWikiServices::getInstance();
434  $authManager = $services->getAuthManager();
435  $permissionManager = $services->getPermissionManager();
436  $returnto = $this->getReturnToParam();
437 
438  /* set up the default links for the personal toolbar */
439  $personal_urls = [];
440 
441  if ( $this->loggedin ) {
442  $personal_urls['userpage'] = $this->buildPersonalPageItem( 'pt-userpage' );
443 
444  // Merge notifications into the personal menu for older skins.
445  if ( $includeNotifications ) {
446  $contentNavigation = $this->buildContentNavigationUrls();
447 
448  $personal_urls += $contentNavigation['notifications'];
449  }
450 
451  $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage );
452  $personal_urls['mytalk'] = [
453  'text' => $this->msg( 'mytalk' )->text(),
454  'href' => &$usertalkUrlDetails['href'],
455  'class' => $usertalkUrlDetails['exists'] ? false : 'new',
456  'exists' => $usertalkUrlDetails['exists'],
457  'active' => ( $usertalkUrlDetails['href'] == $pageurl ),
458  'icon' => 'userTalk'
459  ];
460  $href = self::makeSpecialUrl( 'Preferences' );
461  $personal_urls['preferences'] = [
462  'text' => $this->msg( 'mypreferences' )->text(),
463  'href' => $href,
464  'active' => ( $href == $pageurl ),
465  'icon' => 'settings'
466  ];
467 
468  if ( $this->getAuthority()->isAllowed( 'viewmywatchlist' ) ) {
469  $href = self::makeSpecialUrl( 'Watchlist' );
470  $personal_urls['watchlist'] = [
471  'text' => $this->msg( 'mywatchlist' )->text(),
472  'href' => $href,
473  'active' => ( $href == $pageurl ),
474  'icon' => 'unStar'
475  ];
476  }
477 
478  # We need to do an explicit check for Special:Contributions, as we
479  # have to match both the title, and the target, which could come
480  # from request values (Special:Contributions?target=Jimbo_Wales)
481  # or be specified in "sub page" form
482  # (Special:Contributions/Jimbo_Wales). The plot
483  # thickens, because the Title object is altered for special pages,
484  # so it doesn't contain the original alias-with-subpage.
485  $origTitle = Title::newFromText( $request->getText( 'title' ) );
486  if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) {
487  list( $spName, $spPar ) =
488  MediaWikiServices::getInstance()->getSpecialPageFactory()->
489  resolveAlias( $origTitle->getText() );
490  $active = $spName == 'Contributions'
491  && ( ( $spPar && $spPar == $this->username )
492  || $request->getText( 'target' ) == $this->username );
493  } else {
494  $active = false;
495  }
496 
497  $href = self::makeSpecialUrlSubpage( 'Contributions', $this->username );
498  $personal_urls['mycontris'] = [
499  'text' => $this->msg( 'mycontris' )->text(),
500  'href' => $href,
501  'active' => $active,
502  'icon' => 'userContributions'
503  ];
504 
505  // if we can't set the user, we can't unset it either
506  if ( $request->getSession()->canSetUser() ) {
507  $personal_urls['logout'] = $this->buildLogoutLinkData();
508  }
509  } else {
510  $useCombinedLoginLink = $this->useCombinedLoginLink();
511  $login_url = $this->buildLoginData( $returnto, $useCombinedLoginLink );
512  $createaccount_url = $this->buildCreateAccountData( $returnto );
513 
514  // No need to show Talk and Contributions to anons if they can't contribute!
515  // TODO: easy way to get anon authority!
516  if ( $permissionManager->groupHasPermission( '*', 'edit' ) ) {
517  // Non interactive placeholder for anonymous users.
518  // It's unstyled by default (black color). Skin that
519  // needs it, can style it using the 'pt-anonuserpage' id.
520  // Skin that does not need it should unset it.
521  $personal_urls['anonuserpage'] = [
522  'text' => $this->msg( 'notloggedin' )->text(),
523  ];
524 
525  // Because of caching, we can't link directly to the IP talk and
526  // contributions pages. Instead we use the special page shortcuts
527  // (which work correctly regardless of caching). This means we can't
528  // determine whether these links are active or not, but since major
529  // skins (MonoBook, Vector) don't use this information, it's not a
530  // huge loss.
531  $personal_urls['anontalk'] = [
532  'text' => $this->msg( 'anontalk' )->text(),
533  'href' => self::makeSpecialUrlSubpage( 'Mytalk', false ),
534  'active' => false,
535  'icon' => 'userTalk'
536  ];
537  $personal_urls['anoncontribs'] = [
538  'text' => $this->msg( 'anoncontribs' )->text(),
539  'href' => self::makeSpecialUrlSubpage( 'Mycontributions', false ),
540  'active' => false,
541  'icon' => 'userContributions'
542  ];
543  }
544 
545  if (
546  $authManager->canCreateAccounts()
547  && $this->getAuthority()->isAllowed( 'createaccount' )
548  && !$useCombinedLoginLink
549  ) {
550  $personal_urls['createaccount'] = $createaccount_url;
551  }
552 
553  if ( $authManager->canAuthenticateNow() ) {
554  // TODO: easy way to get anon authority
555  $key = $permissionManager->groupHasPermission( '*', 'read' )
556  ? 'login'
557  : 'login-private';
558  $personal_urls[$key] = $login_url;
559  }
560  }
561 
562  $this->getHookRunner()->onPersonalUrls( $personal_urls, $title, $this );
563 
564  return $personal_urls;
565  }
566 
573  protected function getReturnToParam() {
574  $title = $this->getTitle();
575  $request = $this->getRequest();
576 
577  # Due to T34276, if a user does not have read permissions,
578  # $this->getTitle() will just give Special:Badtitle, which is
579  # not especially useful as a returnto parameter. Use the title
580  # from the request instead, if there was one.
581  if ( $this->getAuthority()->isAllowed( 'read' ) ) {
582  $page = $title;
583  } else {
584  $page = Title::newFromText( $request->getVal( 'title', '' ) );
585  }
586  $page = $request->getVal( 'returnto', $page );
587  $returnto = [];
588  if ( strval( $page ) !== '' ) {
589  $returnto['returnto'] = $page;
590  $query = $request->getVal( 'returntoquery', $this->thisquery );
591  $paramsArray = wfCgiToArray( $query );
592  $query = wfArrayToCgi( $paramsArray );
593  if ( $query != '' ) {
594  $returnto['returntoquery'] = $query;
595  }
596  }
597 
598  return $returnto;
599  }
600 
607  protected function useCombinedLoginLink() {
608  $services = MediaWikiServices::getInstance();
609  $authManager = $services->getAuthManager();
610  $useCombinedLoginLink = $this->getConfig()->get( 'UseCombinedLoginLink' );
611  if ( !$authManager->canCreateAccounts() || !$authManager->canAuthenticateNow() ) {
612  // don't show combined login/signup link if one of those is actually not available
613  $useCombinedLoginLink = false;
614  }
615 
616  return $useCombinedLoginLink;
617  }
618 
628  protected function buildLoginData( $returnto, $useCombinedLoginLink ) {
629  $title = $this->getTitle();
630 
631  $loginlink = $this->getAuthority()->isAllowed( 'createaccount' )
632  && $useCombinedLoginLink ? 'nav-login-createaccount' : 'pt-login';
633 
634  $login_url = [
635  'text' => $this->msg( $loginlink )->text(),
636  'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
637  'active' => $title->isSpecial( 'Userlogin' )
638  || $title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink,
639  'icon' => 'logIn'
640  ];
641 
642  return $login_url;
643  }
644 
653  protected function getPortletData( $name, array $items ) {
654  // Monobook and Vector historically render this portal as an element with ID p-cactions
655  // This inconsistency is regretful from a code point of view
656  // However this ensures compatibility with gadgets.
657  // In future we should port p-#cactions to #p-actions and drop this rename.
658  if ( $name === 'actions' ) {
659  $name = 'cactions';
660  }
661 
662  // user-menu is the new personal tools, without the notifications.
663  // A lot of user code and gadgets relies on it being named personal.
664  // This allows it to function as a drop-in replacement.
665  if ( $name === 'user-menu' ) {
666  $name = 'personal';
667  }
668 
669  $id = Sanitizer::escapeIdForAttribute( "p-$name" );
670  $data = [
671  'id' => $id,
672  'class' => 'mw-portlet ' . Sanitizer::escapeClass( "mw-portlet-$name" ),
673  'html-tooltip' => Linker::tooltip( $id ),
674  'html-items' => '',
675  // Will be populated by SkinAfterPortlet hook.
676  'html-after-portal' => '',
677  'html-before-portal' => '',
678  ];
679  // Run the SkinAfterPortlet
680  // hook and if content is added appends it to the html-after-portal
681  // for output.
682  // Currently in production this supports the wikibase 'edit' link.
683  $content = $this->getAfterPortlet( $name );
684  if ( $content !== '' ) {
685  $data['html-after-portal'] = Html::rawElement(
686  'div',
687  [
688  'class' => [
689  'after-portlet',
690  Sanitizer::escapeClass( "after-portlet-$name" ),
691  ],
692  ],
693  $content
694  );
695  }
696 
697  foreach ( $items as $key => $item ) {
698  $data['html-items'] .= $this->makeListItem( $key, $item );
699  }
700 
701  $data['label'] = $this->getPortletLabel( $name );
702  $data['class'] .= ( count( $items ) === 0 && $content === '' )
703  ? ' emptyPortlet' : '';
704  return $data;
705  }
706 
711  private function getPortletLabel( $name ) {
712  // For historic reasons for some menu items,
713  // there is no language key corresponding with its menu key.
714  $mappings = [
715  'tb' => 'toolbox',
716  'personal' => 'personaltools',
717  'lang' => 'otherlanguages',
718  ];
719  $msgObj = $this->msg( $mappings[ $name ] ?? $name );
720  // If no message exists fallback to plain text (T252727)
721  $labelText = $msgObj->exists() ? $msgObj->text() : $name;
722  return $labelText;
723  }
724 
728  private function getPortletsTemplateData() {
729  $portlets = [];
730  $contentNavigation = $this->buildContentNavigationUrls();
731  $sidebar = [];
732  $sidebarData = $this->buildSidebar();
733  foreach ( $sidebarData as $name => $items ) {
734  if ( is_array( $items ) ) {
735  // Numeric strings gets an integer when set as key, cast back - T73639
736  $name = (string)$name;
737  switch ( $name ) {
738  // ignore search
739  case 'SEARCH':
740  break;
741  // Map toolbox to `tb` id.
742  case 'TOOLBOX':
743  $sidebar[] = $this->getPortletData( 'tb', $items );
744  break;
745  // Languages is no longer be tied to sidebar
746  case 'LANGUAGES':
747  // The language portal will be added provided either
748  // languages exist or there is a value in html-after-portal
749  // for example to show the add language wikidata link (T252800)
750  $portal = $this->getPortletData( 'lang', $items );
751  if ( count( $items ) || $portal['html-after-portal'] ) {
752  $portlets['data-languages'] = $portal;
753  }
754  break;
755  default:
756  $sidebar[] = $this->getPortletData( $name, $items );
757  break;
758  }
759  }
760  }
761 
762  foreach ( $contentNavigation as $name => $items ) {
763  if ( $name === 'user-menu' ) {
764  $items = $this->getPersonalToolsForMakeListItem( $items, true );
765  }
766 
767  $portlets['data-' . $name] = $this->getPortletData( $name, $items );
768  }
769 
770  // A menu that includes the notifications. This will be deprecated in future versions
771  // of the skin API spec.
772  $portlets['data-personal'] = $this->getPortletData(
773  'personal',
775  $this->injectLegacyMenusIntoPersonalTools( $contentNavigation )
776  )
777  );
778 
779  return [
780  'data-portlets' => $portlets,
781  'data-portlets-sidebar' => [
782  'data-portlets-first' => $sidebar[0] ?? null,
783  'array-portlets-rest' => array_slice( $sidebar, 1 ),
784  ],
785  ];
786  }
787 
792  private function getFooterTemplateData(): array {
793  $data = [];
794  foreach ( $this->getFooterLinks() as $category => $links ) {
795  $items = [];
796  $rowId = "footer-$category";
797 
798  foreach ( $links as $key => $link ) {
799  // Link may be null. If so don't include it.
800  if ( $link ) {
801  $items[] = [
802  // Monobook uses name rather than id.
803  // We may want to change monobook to adhere to the same contract however.
804  'name' => $key,
805  'id' => "$rowId-$key",
806  'html' => $link,
807  ];
808  }
809  }
810 
811  $data['data-' . $category] = [
812  'id' => $rowId,
813  'className' => null,
814  'array-items' => $items
815  ];
816  }
817 
818  // If footer icons are enabled append to the end of the rows
819  $footerIcons = $this->getFooterIcons();
820 
821  if ( count( $footerIcons ) > 0 ) {
822  $icons = [];
823  foreach ( $footerIcons as $blockName => $blockIcons ) {
824  $html = '';
825  foreach ( $blockIcons as $key => $icon ) {
826  $html .= $this->makeFooterIcon( $icon );
827  }
828  // For historic reasons this mimics the `icononly` option
829  // for BaseTemplate::getFooterIcons. Empty rows should not be output.
830  if ( $html ) {
831  $block = htmlspecialchars( $blockName );
832  $icons[] = [
833  'name' => $block,
834  'id' => 'footer-' . $block . 'ico',
835  'html' => $html,
836  ];
837  }
838  }
839 
840  // Empty rows should not be output.
841  // This is how Vector has behaved historically but we can revisit later if necessary.
842  if ( count( $icons ) > 0 ) {
843  $data['data-icons'] = [
844  'id' => 'footer-icons',
845  'className' => 'noprint',
846  'array-items' => $icons,
847  ];
848  }
849  }
850 
851  return [
852  'data-footer' => $data,
853  ];
854  }
855 
859  private function getLogoData(): array {
861  // check if the logo supports variants
862  $variantsLogos = $logoData['variants'] ?? null;
863  if ( $variantsLogos ) {
864  $preferred = $this->getOutput()->getTitle()
865  ->getPageViewLanguage()->getCode();
866  $variantOverrides = $variantsLogos[$preferred] ?? null;
867  // Overrides the logo
868  if ( $variantOverrides ) {
869  foreach ( $variantOverrides as $key => $val ) {
870  $logoData[$key] = $val;
871  }
872  }
873  }
874  return $logoData;
875  }
876 
880  private function buildSearchProps(): array {
881  $config = $this->getConfig();
882  $searchButtonAttributes = [
883  'class' => 'searchButton'
884  ];
885  $fallbackButtonAttributes = [
886  'class' => 'searchButton mw-fallbackSearchButton'
887  ];
888  $buttonAttributes = [
889  'type' => 'submit',
890  ];
891 
892  $props = [
893  'form-action' => $config->get( 'Script' ),
894  'html-button-search-fallback' => $this->makeSearchButton(
895  'fulltext',
896  $fallbackButtonAttributes + [
897  'id' => 'mw-searchButton',
898  ]
899  ),
900  'html-button-search' => $this->makeSearchButton(
901  'go',
902  $searchButtonAttributes + [
903  'id' => 'searchButton',
904  ]
905  ),
906  'html-input' => $this->makeSearchInput( [ 'id' => 'searchInput' ] ),
907  'msg-search' => $this->msg( 'search' )->text(),
908  'page-title' => $this->getSearchPageTitle()->getPrefixedDBkey(),
909  // @since 1.38
910  'html-button-go-attributes' => Html::expandAttributes(
911  $searchButtonAttributes + $buttonAttributes + [
912  'name' => 'go',
913  ] + Linker::tooltipAndAccesskeyAttribs( 'search-go' )
914  ),
915  // @since 1.38
916  'html-button-fulltext-attributes' => Html::expandAttributes(
917  $fallbackButtonAttributes + $buttonAttributes + [
918  'name' => 'fulltext'
919  ] + Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' )
920  ),
921  // @since 1.38
922  'html-input-attributes' => Html::expandAttributes(
923  $this->getSearchInputAttributes( [] )
924  ),
925  ];
926 
927  return $props;
928  }
929 
939  final protected function buildLogoutLinkData() {
940  $title = $this->getTitle();
941  $returnto = $this->getReturnToParam();
942 
943  return [
944  'text' => $this->msg( 'pt-userlogout' )->text(),
945  'data-mw' => 'interface',
946  'href' => self::makeSpecialUrl( 'Userlogout',
947  // Note: userlogout link must always contain an & character, otherwise we might not be able
948  // to detect a buggy precaching proxy (T19790)
949  ( $title->isSpecial( 'Preferences' ) ? [] : $returnto ) ),
950  'active' => false,
951  'icon' => 'logOut'
952  ];
953  }
954 
962  protected function buildCreateAccountData( $returnto ) {
963  $title = $this->getTitle();
964 
965  $createaccount_url = [
966  'text' => $this->msg( 'pt-createaccount' )->text(),
967  'href' => self::makeSpecialUrl( 'CreateAccount', $returnto ),
968  'active' => $title->isSpecial( 'CreateAccount' ),
969  'icon' => 'userAvatar'
970  ];
971 
972  return $createaccount_url;
973  }
974 
980  protected function buildPersonalPageItem( $id = 'pt-userpage' ): array {
981  // Build the personal page link array.
982  return [
983  'text' => $this->username,
984  'id' => $id,
985  'href' => &$this->userpageUrlDetails['href'],
986  'link-class' => $this->userpageUrlDetails['exists'] ? [] : [ 'new' ],
987  'exists' => $this->userpageUrlDetails['exists'],
988  'active' => ( $this->userpageUrlDetails['href'] == $this->getTitle()->getLocalURL() ),
989  'icon' => 'userAvatar',
990  'dir' => 'auto'
991  ];
992  }
993 
1007  public function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) {
1008  $classes = [];
1009  if ( $selected ) {
1010  $classes[] = 'selected';
1011  }
1012  $exists = true;
1013  if ( $checkEdit && !$title->isKnown() ) {
1014  $classes[] = 'new';
1015  $exists = false;
1016  if ( $query !== '' ) {
1017  $query = 'action=edit&redlink=1&' . $query;
1018  } else {
1019  $query = 'action=edit&redlink=1';
1020  }
1021  }
1022 
1023  $services = MediaWikiServices::getInstance();
1024  $linkClass = $services->getLinkRenderer()->getLinkClasses( $title );
1025 
1026  if ( $message instanceof MessageSpecifier ) {
1027  $msg = new Message( $message );
1028  $message = $message->getKey();
1029  } else {
1030  // wfMessageFallback will nicely accept $message as an array of fallbacks
1031  // or just a single key
1032  $msg = wfMessageFallback( $message );
1033  if ( is_array( $message ) ) {
1034  // for hook compatibility just keep the last message name
1035  $message = end( $message );
1036  }
1037  }
1038  $msg->setContext( $this->getContext() );
1039  if ( $msg->exists() ) {
1040  $text = $msg->text();
1041  } else {
1042  $text = $services->getLanguageConverterFactory()
1043  ->getLanguageConverter( $services->getContentLanguage() )
1044  ->convertNamespace(
1045  $services->getNamespaceInfo()
1046  ->getSubject( $title->getNamespace() )
1047  );
1048  }
1049 
1050  $result = [
1051  'class' => implode( ' ', $classes ),
1052  'text' => $text,
1053  'href' => $title->getLocalURL( $query ),
1054  'exists' => $exists,
1055  'primary' => true ];
1056  if ( $linkClass !== '' ) {
1057  $result['link-class'] = $linkClass;
1058  }
1059 
1060  return $result;
1061  }
1062 
1068  private function makeTalkUrlDetails( $name, $urlaction = '' ) {
1069  $title = Title::newFromText( $name );
1070  if ( !is_object( $title ) ) {
1071  throw new MWException( __METHOD__ . " given invalid pagename $name" );
1072  }
1073  $title = $title->getTalkPage();
1074  self::checkTitle( $title, $name );
1075  return [
1076  'href' => $title->getLocalURL( $urlaction ),
1077  'exists' => $title->isKnown(),
1078  ];
1079  }
1080 
1090  private function getWatchLinkAttrs(
1091  string $mode, Authority $performer, Title $title, ?string $action, bool $onPage
1092  ): array {
1093  $class = 'mw-watchlink ' . (
1094  $onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : ''
1095  );
1096 
1097  // Add class identifying the page is temporarily watched, if applicable.
1098  if ( $this->getConfig()->get( 'WatchlistExpiry' ) &&
1099  MediaWikiServices::getInstance()->getWatchlistManager()->isTempWatched( $performer, $title )
1100  ) {
1101  $class .= ' mw-watchlink-temp';
1102  }
1103 
1104  return [
1105  'class' => $class,
1106  // uses 'watch' or 'unwatch' message
1107  'text' => $this->msg( $mode )->text(),
1108  'href' => $title->getLocalURL( [ 'action' => $mode ] ),
1109  // Set a data-mw=interface attribute, which the mediawiki.page.ajax
1110  // module will look for to make sure it's a trusted link
1111  'data' => [
1112  'mw' => 'interface',
1113  ],
1114  ];
1115  }
1116 
1126  protected function runOnSkinTemplateNavigationHooks( SkinTemplate $skin, &$content_navigation ) {
1127  $title = $this->getRelevantTitle();
1128  if ( $title->canExist() ) {
1129  $this->getHookRunner()->onSkinTemplateNavigation( $skin, $content_navigation );
1130  } else {
1131  $this->getHookRunner()->onSkinTemplateNavigation__SpecialPage(
1132  $skin, $content_navigation );
1133  }
1134 
1135  // Equiv to SkinTemplateContentActions, run
1136  $this->getHookRunner()->onSkinTemplateNavigation__Universal(
1137  $skin, $content_navigation );
1138  }
1139 
1174  protected function buildContentNavigationUrls() {
1175  // Display tabs for the relevant title rather than always the title itself
1176  $title = $this->getRelevantTitle();
1177  $onPage = $title->equals( $this->getTitle() );
1178 
1179  $out = $this->getOutput();
1180  $request = $this->getRequest();
1181  $performer = $this->getAuthority();
1182  $action = $this->getAction();
1183  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1184 
1185  $content_navigation = [
1186  // Modern keys: Please ensure these get unset inside Skin::prepareQuickTemplate
1187  'user-interface-preferences' => [],
1188  'user-page' => $this->loggedin ? [
1189  'userpage' => $this->buildPersonalPageItem( 'pt-userpage-2' )
1190  ] : [],
1191  'user-menu' => $this->buildPersonalUrls( false ),
1192  'notifications' => [],
1193  // Legacy keys
1194  'namespaces' => [],
1195  'views' => [],
1196  'actions' => [],
1197  'variants' => []
1198  ];
1199 
1200  $userCanRead = $this->getAuthority()->probablyCan( 'read', $title );
1201 
1202  // Checks if page is some kind of content
1203  if ( $title->canExist() ) {
1204  // Gets page objects for the related namespaces
1205  $subjectPage = $title->getSubjectPage();
1206  $talkPage = $title->getTalkPage();
1207 
1208  // Determines if this is a talk page
1209  $isTalk = $title->isTalkPage();
1210 
1211  // Generates XML IDs from namespace names
1212  $subjectId = $title->getNamespaceKey( '' );
1213 
1214  if ( $subjectId == 'main' ) {
1215  $talkId = 'talk';
1216  } else {
1217  $talkId = "{$subjectId}_talk";
1218  }
1219 
1220  $skname = $this->skinname;
1221 
1222  // Adds namespace links
1223  if ( $subjectId === 'user' ) {
1224  $subjectMsg = wfMessage( 'nstab-user', $subjectPage->getRootText() );
1225  } else {
1226  $subjectMsg = [ "nstab-$subjectId" ];
1227  }
1228  if ( $subjectPage->isMainPage() ) {
1229  array_unshift( $subjectMsg, 'mainpage-nstab' );
1230  }
1231 
1232  $content_navigation['namespaces'][$subjectId] = $this->tabAction(
1233  $subjectPage, $subjectMsg, !$isTalk, '', $userCanRead
1234  );
1235  $content_navigation['namespaces'][$subjectId]['context'] = 'subject';
1236  $content_navigation['namespaces'][$talkId] = $this->tabAction(
1237  $talkPage, [ "nstab-$talkId", 'talk' ], $isTalk, '', $userCanRead
1238  );
1239  $content_navigation['namespaces'][$talkId]['context'] = 'talk';
1240 
1241  if ( $userCanRead ) {
1242  // Adds "view" view link
1243  if ( $title->isKnown() ) {
1244  $content_navigation['views']['view'] = $this->tabAction(
1245  $isTalk ? $talkPage : $subjectPage,
1246  [ "$skname-view-view", 'view' ],
1247  ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true
1248  );
1249  // signal to hide this from simple content_actions
1250  $content_navigation['views']['view']['redundant'] = true;
1251  }
1252 
1253  $page = $this->canUseWikiPage() ? $this->getWikiPage() : false;
1254  $isRemoteContent = $page && !$page->isLocal();
1255 
1256  // If it is a non-local file, show a link to the file in its own repository
1257  // @todo abstract this for remote content that isn't a file
1258  if ( $isRemoteContent ) {
1259  $content_navigation['views']['view-foreign'] = [
1260  'class' => '',
1261  'text' => wfMessageFallback( "$skname-view-foreign", 'view-foreign' )->
1262  setContext( $this->getContext() )->
1263  params( $page->getWikiDisplayName() )->text(),
1264  'href' => $page->getSourceURL(),
1265  'primary' => false,
1266  ];
1267  }
1268 
1269  // Checks if user can edit the current page if it exists or create it otherwise
1270  if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
1271  // Builds CSS class for talk page links
1272  $isTalkClass = $isTalk ? ' istalk' : '';
1273  // Whether the user is editing the page
1274  $isEditing = $onPage && ( $action == 'edit' || $action == 'submit' );
1275  // Whether to show the "Add a new section" tab
1276  // Checks if this is a current rev of talk page and is not forced to be hidden
1277  $showNewSection = !$out->forceHideNewSectionLink()
1278  && ( ( $isTalk && $out->isRevisionCurrent() ) || $out->showNewSectionLink() );
1279  $section = $request->getVal( 'section' );
1280 
1281  if ( $title->exists()
1282  || ( $title->inNamespace( NS_MEDIAWIKI )
1283  && $title->getDefaultMessageText() !== false
1284  )
1285  ) {
1286  $msgKey = $isRemoteContent ? 'edit-local' : 'edit';
1287  } else {
1288  $msgKey = $isRemoteContent ? 'create-local' : 'create';
1289  }
1290  $content_navigation['views']['edit'] = [
1291  'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection )
1292  ? 'selected'
1293  : ''
1294  ) . $isTalkClass,
1295  'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey )
1296  ->setContext( $this->getContext() )->text(),
1297  'href' => $title->getLocalURL( $this->editUrlOptions() ),
1298  'primary' => !$isRemoteContent, // don't collapse this in vector
1299  ];
1300 
1301  // section link
1302  if ( $showNewSection ) {
1303  // Adds new section link
1304  // $content_navigation['actions']['addsection']
1305  $content_navigation['views']['addsection'] = [
1306  'class' => ( $isEditing && $section == 'new' ) ? 'selected' : false,
1307  'text' => wfMessageFallback( "$skname-action-addsection", 'addsection' )
1308  ->setContext( $this->getContext() )->text(),
1309  'href' => $title->getLocalURL( 'action=edit&section=new' )
1310  ];
1311  }
1312  // Checks if the page has some kind of viewable source content
1313  } elseif ( $title->hasSourceText() ) {
1314  // Adds view source view link
1315  $content_navigation['views']['viewsource'] = [
1316  'class' => ( $onPage && $action == 'edit' ) ? 'selected' : false,
1317  'text' => wfMessageFallback( "$skname-action-viewsource", 'viewsource' )
1318  ->setContext( $this->getContext() )->text(),
1319  'href' => $title->getLocalURL( $this->editUrlOptions() ),
1320  'primary' => true, // don't collapse this in vector
1321  ];
1322  }
1323 
1324  // Checks if the page exists
1325  if ( $title->exists() ) {
1326  // Adds history view link
1327  $content_navigation['views']['history'] = [
1328  'class' => ( $onPage && $action == 'history' ) ? 'selected' : false,
1329  'text' => wfMessageFallback( "$skname-view-history", 'history_short' )
1330  ->setContext( $this->getContext() )->text(),
1331  'href' => $title->getLocalURL( 'action=history' ),
1332  ];
1333 
1334  if ( $this->getAuthority()->probablyCan( 'delete', $title ) ) {
1335  $content_navigation['actions']['delete'] = [
1336  'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false,
1337  'text' => wfMessageFallback( "$skname-action-delete", 'delete' )
1338  ->setContext( $this->getContext() )->text(),
1339  'href' => $title->getLocalURL( 'action=delete' )
1340  ];
1341  }
1342 
1343  if ( $this->getAuthority()->probablyCan( 'move', $title ) ) {
1344  $moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() );
1345  $content_navigation['actions']['move'] = [
1346  'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false,
1347  'text' => wfMessageFallback( "$skname-action-move", 'move' )
1348  ->setContext( $this->getContext() )->text(),
1349  'href' => $moveTitle->getLocalURL()
1350  ];
1351  }
1352  } else {
1353  // article doesn't exist or is deleted
1354  if ( $this->getAuthority()->probablyCan( 'deletedhistory', $title ) ) {
1355  $n = $title->getDeletedEditsCount();
1356  if ( $n ) {
1357  $undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() );
1358  // If the user can't undelete but can view deleted
1359  // history show them a "View .. deleted" tab instead.
1360  $msgKey = $this->getAuthority()->probablyCan( 'undelete', $title ) ?
1361  'undelete' : 'viewdeleted';
1362  $content_navigation['actions']['undelete'] = [
1363  'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false,
1364  'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" )
1365  ->setContext( $this->getContext() )->numParams( $n )->text(),
1366  'href' => $undelTitle->getLocalURL()
1367  ];
1368  }
1369  }
1370  }
1371 
1372  if ( $this->getAuthority()->probablyCan( 'protect', $title ) &&
1373  $title->getRestrictionTypes() &&
1374  $permissionManager->getNamespaceRestrictionLevels(
1375  $title->getNamespace(),
1376  $performer->getUser()
1377  ) !== [ '' ]
1378  ) {
1379  $mode = $title->isProtected() ? 'unprotect' : 'protect';
1380  $content_navigation['actions'][$mode] = [
1381  'class' => ( $onPage && $action == $mode ) ? 'selected' : false,
1382  'text' => wfMessageFallback( "$skname-action-$mode", $mode )
1383  ->setContext( $this->getContext() )->text(),
1384  'href' => $title->getLocalURL( "action=$mode" )
1385  ];
1386  }
1387 
1388  // Checks if the user is logged in
1389  if ( $this->loggedin && $this->getAuthority()
1390  ->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' )
1391  ) {
1401  $mode = MediaWikiServices::getInstance()->getWatchlistManager()
1402  ->isWatched( $performer, $title ) ? 'unwatch' : 'watch';
1403 
1404  // Add the watch/unwatch link.
1405  $content_navigation['actions'][$mode] = $this->getWatchLinkAttrs(
1406  $mode,
1407  $performer,
1408  $title,
1409  $action,
1410  $onPage
1411  );
1412  }
1413  }
1414 
1415  // Add language variants
1416  $languageConverterFactory = MediaWikiServices::getInstance()->getLanguageConverterFactory();
1417 
1418  if ( $userCanRead && !$languageConverterFactory->isConversionDisabled() ) {
1419  $pageLang = $title->getPageLanguage();
1420  $converter = $languageConverterFactory
1421  ->getLanguageConverter( $pageLang );
1422  // Checks that language conversion is enabled and variants exist
1423  // And if it is not in the special namespace
1424  if ( $converter->hasVariants() ) {
1425  // Gets list of language variants
1426  $variants = $converter->getVariants();
1427  // Gets preferred variant (note that user preference is
1428  // only possible for wiki content language variant)
1429  $preferred = $converter->getPreferredVariant();
1430  if ( $action === 'view' ) {
1431  $params = $request->getQueryValues();
1432  unset( $params['title'] );
1433  } else {
1434  $params = [];
1435  }
1436  // Loops over each variant
1437  foreach ( $variants as $code ) {
1438  // Gets variant name from language code
1439  $varname = $pageLang->getVariantname( $code );
1440  // Appends variant link
1441  $content_navigation['variants'][] = [
1442  'class' => ( $code == $preferred ) ? 'selected' : false,
1443  'text' => $varname,
1444  'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ),
1445  'lang' => LanguageCode::bcp47( $code ),
1446  'hreflang' => LanguageCode::bcp47( $code ),
1447  ];
1448  }
1449  }
1450  }
1451  } else {
1452  // If it's not content, and a request URL is set it's got to be a special page
1453  try {
1454  $url = $request->getRequestURL();
1455  } catch ( MWException $e ) {
1456  $url = false;
1457  }
1458  $content_navigation['namespaces']['special'] = [
1459  'class' => 'selected',
1460  'text' => $this->msg( 'nstab-special' )->text(),
1461  'href' => $url, // @see: T4457, T4510
1462  'context' => 'subject'
1463  ];
1464  }
1465  $this->runOnSkinTemplateNavigationHooks( $this, $content_navigation );
1466 
1467  // Setup xml ids and tooltip info
1468  foreach ( $content_navigation as $section => &$links ) {
1469  foreach ( $links as $key => &$link ) {
1470  // Allow links to set their own id for backwards compatibility reasons.
1471  if ( isset( $link['id'] ) ) {
1472  continue;
1473  }
1474  $xmlID = $key;
1475  if ( isset( $link['context'] ) && $link['context'] == 'subject' ) {
1476  $xmlID = 'ca-nstab-' . $xmlID;
1477  } elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) {
1478  $xmlID = 'ca-talk';
1479  $link['rel'] = 'discussion';
1480  } elseif ( $section == 'variants' ) {
1481  $xmlID = 'ca-varlang-' . $xmlID;
1482  } else {
1483  $xmlID = 'ca-' . $xmlID;
1484  }
1485  $link['id'] = $xmlID;
1486  }
1487  }
1488 
1489  # We don't want to give the watch tab an accesskey if the
1490  # page is being edited, because that conflicts with the
1491  # accesskey on the watch checkbox. We also don't want to
1492  # give the edit tab an accesskey, because that's fairly
1493  # superfluous and conflicts with an accesskey (Ctrl-E) often
1494  # used for editing in Safari.
1495  if ( in_array( $action, [ 'edit', 'submit' ] ) ) {
1496  if ( isset( $content_navigation['views']['edit'] ) ) {
1497  $content_navigation['views']['edit']['tooltiponly'] = true;
1498  }
1499  if ( isset( $content_navigation['actions']['watch'] ) ) {
1500  $content_navigation['actions']['watch']['tooltiponly'] = true;
1501  }
1502  if ( isset( $content_navigation['actions']['unwatch'] ) ) {
1503  $content_navigation['actions']['unwatch']['tooltiponly'] = true;
1504  }
1505  }
1506 
1507  return $content_navigation;
1508  }
1509 
1515  private function buildContentActionUrls( $content_navigation ) {
1516  // content_actions has been replaced with content_navigation for backwards
1517  // compatibility and also for skins that just want simple tabs content_actions
1518  // is now built by flattening the content_navigation arrays into one
1519 
1520  $content_actions = [];
1521 
1522  foreach ( $content_navigation as $navigation => $links ) {
1523  foreach ( $links as $key => $value ) {
1524  if ( isset( $value['redundant'] ) && $value['redundant'] ) {
1525  // Redundant tabs are dropped from content_actions
1526  continue;
1527  }
1528 
1529  // content_actions used to have ids built using the "ca-$key" pattern
1530  // so the xmlID based id is much closer to the actual $key that we want
1531  // for that reason we'll just strip out the ca- if present and use
1532  // the latter potion of the "id" as the $key
1533  if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) {
1534  $key = substr( $value['id'], 3 );
1535  }
1536 
1537  if ( isset( $content_actions[$key] ) ) {
1538  wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " .
1539  "content_navigation into content_actions." );
1540  continue;
1541  }
1542 
1543  $content_actions[$key] = $value;
1544  }
1545  }
1546 
1547  return $content_actions;
1548  }
1549 
1555  protected function getNameSpaceKey() {
1556  return $this->getTitle()->getNamespaceKey();
1557  }
1558 
1567  final protected function injectLegacyMenusIntoPersonalTools(
1568  array $contentNavigation
1569  ): array {
1570  $userMenu = $contentNavigation['user-menu'] ?? [];
1571  // userpage is only defined for logged-in users, and wfArrayInsertAfter requires the
1572  // $after parameter to be a known key in the array.
1573  if ( isset( $contentNavigation['user-menu']['userpage'] ) && isset( $contentNavigation['notifications'] ) ) {
1574  $userMenu = wfArrayInsertAfter(
1575  $userMenu,
1576  $contentNavigation['notifications'],
1577  'userpage'
1578  );
1579  }
1580  if ( isset( $contentNavigation['user-interface-preferences'] ) ) {
1581  return array_merge(
1582  $contentNavigation['user-interface-preferences'],
1583  $userMenu
1584  );
1585  }
1586  return $userMenu;
1587  }
1588 
1598  array $contentNavigation
1599  ): array {
1600  if ( isset( $contentNavigation['user-menu'] ) ) {
1601  return $this->injectLegacyMenusIntoPersonalTools( $contentNavigation );
1602  }
1603  return [];
1604  }
1605 
1606 }
Skin\prepareSubtitle
prepareSubtitle()
Prepare the subtitle of the page for output in the skin if one has been set.
Definition: Skin.php:2416
SkinTemplate\makePersonalToolsList
makePersonalToolsList( $personalTools=null, $options=[])
Get the HTML for the personal tools list Please ensure setupTemplateContext is called before calling ...
Definition: SkinTemplate.php:390
Skin\$skinname
string null $skinname
Definition: Skin.php:55
wfArrayInsertAfter
wfArrayInsertAfter(array $array, array $insert, $after)
Insert array into another array after the specified KEY
Definition: GlobalFunctions.php:189
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
SkinTemplate\makeSkinTemplatePersonalUrls
makeSkinTemplatePersonalUrls(array $contentNavigation)
Build the personal urls array.
Definition: SkinTemplate.php:1597
Skin\getAction
getAction()
Optimization.
Definition: Skin.php:2524
SkinTemplate\useCombinedLoginLink
useCombinedLoginLink()
Returns if a combined login/signup link will be used.
Definition: SkinTemplate.php:607
Skin\editUrlOptions
editUrlOptions()
Return URL options for the 'edit page' link.
Definition: Skin.php:1092
SkinTemplate\setupTemplateForOutput
setupTemplateForOutput()
Definition: SkinTemplate.php:65
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:1209
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
Skin\getSiteNotice
getSiteNotice()
Definition: Skin.php:1853
wfMessageFallback
wfMessageFallback(... $keys)
This function accepts multiple message keys and returns a message instance for the first message whic...
Definition: GlobalFunctions.php:1198
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
SkinTemplate\getPortletsTemplateData
getPortletsTemplateData()
Definition: SkinTemplate.php:728
Skin\buildFeedUrls
buildFeedUrls()
Build data structure representing syndication links.
Definition: Skin.php:1502
SkinTemplate\setupTemplate
setupTemplate( $classname)
Create the template engine object; we feed it a bunch of data and eventually it spits out some HTML.
Definition: SkinTemplate.php:58
Skin\$options
array $options
Skin options passed into constructor.
Definition: Skin.php:60
SkinTemplate\buildContentNavigationUrls
buildContentNavigationUrls()
a structured array of links usually used for the tabs in a skin
Definition: SkinTemplate.php:1174
Profiler\instance
static instance()
Singleton.
Definition: Profiler.php:69
SkinTemplate\getPortletLabel
getPortletLabel( $name)
Definition: SkinTemplate.php:711
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:200
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:2027
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
MessageSpecifier
Definition: MessageSpecifier.php:24
Skin\checkTitle
static checkTitle(&$title, $name)
make sure we have some title to operate on
Definition: Skin.php:1241
SkinTemplate\$userpageUrlDetails
$userpageUrlDetails
Definition: SkinTemplate.php:48
MWDebug\getHTMLDebugLog
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition: MWDebug.php:643
Skin\makeSpecialUrl
static makeSpecialUrl( $name, $urlaction='', $proto=null)
Make a URL for a Special Page using the given query and protocol.
Definition: Skin.php:1167
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:476
SkinTemplate\getLogoData
getLogoData()
Definition: SkinTemplate.php:859
SkinTemplate\$titletxt
$titletxt
Definition: SkinTemplate.php:42
Skin\$action
string $action
cached action for cheap lookup
Definition: Skin.php:80
Skin\wrapHTML
wrapHTML( $title, $html)
Wrap the body text with language information and identifiable element.
Definition: Skin.php:2540
Skin\makeSpecialUrlSubpage
static makeSpecialUrlSubpage( $name, $subpage, $urlaction='')
Definition: Skin.php:1182
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1175
SkinTemplate\$thispage
$thispage
Definition: SkinTemplate.php:41
Skin\getJsConfigVars
getJsConfigVars()
Returns array of config variables that should be added only to this skin for use in JavaScript.
Definition: Skin.php:2481
SkinTemplate\getReturnToParam
getReturnToParam()
Builds query params for the page to return to, used when building links.
Definition: SkinTemplate.php:573
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:107
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:973
ContextSource\canUseWikiPage
canUseWikiPage()
Check whether a WikiPage object can be get with getWikiPage().
Definition: ContextSource.php:103
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:81
ContextSource\getUser
getUser()
Definition: ContextSource.php:136
ContextSource\getTitle
getTitle()
Definition: ContextSource.php:90
SkinTemplate\getPortletData
getPortletData( $name, array $items)
Definition: SkinTemplate.php:653
Skin\makeListItem
makeListItem( $key, $item, $options=[])
Generates a list item for a navigation, portlet, portal, sidebar...
Definition: Skin.php:2245
Skin\bottomScripts
bottomScripts()
This gets called shortly before the "</body>" tag.
Definition: Skin.php:697
Linker\tooltipAndAccesskeyAttribs
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2181
SkinTemplate\$userpage
$userpage
Definition: SkinTemplate.php:43
Skin\afterContentHook
afterContentHook()
This runs a hook to allow extensions placing their stuff after content and article metadata (e....
Definition: Skin.php:671
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:153
SkinTemplate\getPersonalToolsList
getPersonalToolsList()
Get the HTML for the p-personal list.
Definition: SkinTemplate.php:375
wfReportTime
wfReportTime( $nonce=null)
Returns a script tag that stores the amount of time it took MediaWiki to handle the request in millis...
Definition: GlobalFunctions.php:1258
Skin\prepareUndeleteLink
prepareUndeleteLink()
Prepare undelete link for output in page.
Definition: Skin.php:2513
MWException
MediaWiki exception.
Definition: MWException.php:29
Skin\buildSidebar
buildSidebar()
Build an array that represents the sidebar(s), the navigation bar among them.
Definition: Skin.php:1541
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:989
Skin\prepareUserLanguageAttributes
prepareUserLanguageAttributes()
Prepare user language attribute links.
Definition: Skin.php:2490
SkinTemplate\buildSearchProps
buildSearchProps()
Definition: SkinTemplate.php:880
SkinTemplate\getFooterIcons
getFooterIcons()
Get template representation of the footer.
Definition: SkinTemplate.php:148
SkinTemplate\tabAction
tabAction( $title, $message, $selected, $query='', $checkEdit=false)
Builds an array with tab definition.
Definition: SkinTemplate.php:1007
Skin\getCategories
getCategories()
Definition: Skin.php:636
Skin\getSearchPageTitle
getSearchPageTitle()
Definition: Skin.php:2566
SkinTemplate\buildContentActionUrls
buildContentActionUrls( $content_navigation)
an array of edit links by default used for the tabs
Definition: SkinTemplate.php:1515
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:126
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:117
SkinTemplate\$username
$username
Definition: SkinTemplate.php:47
wfCgiToArray
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
Definition: GlobalFunctions.php:375
Skin\getLanguages
getLanguages()
Generates array of language links for the current page.
Definition: Skin.php:1271
SkinTemplate\$loggedin
$loggedin
Definition: SkinTemplate.php:46
$title
$title
Definition: testCompression.php:38
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:886
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:63
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:567
SkinTemplate\buildPersonalUrls
buildPersonalUrls(bool $includeNotifications=true)
build array of urls for personal toolbar Please ensure setupTemplateContext is called before calling ...
Definition: SkinTemplate.php:429
SkinTemplate\getStructuredPersonalTools
getStructuredPersonalTools()
Get personal tools for the user.
Definition: SkinTemplate.php:414
SkinTemplate\$thisquery
$thisquery
Definition: SkinTemplate.php:44
SkinTemplate\buildCreateAccountData
buildCreateAccountData( $returnto)
Build "Create Account" link.
Definition: SkinTemplate.php:962
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
SkinTemplate\prepareQuickTemplate
prepareQuickTemplate()
initialize various variables and generate the template
Definition: SkinTemplate.php:202
SkinTemplate\setupTemplateContext
setupTemplateContext()
Setup class properties that are necessary prior to calling setupTemplateForOutput.
Definition: SkinTemplate.php:87
Skin\getRelevantTitle
getRelevantTitle()
Return the "relevant" title.
Definition: Skin.php:415
$content
$content
Definition: router.php:76
Skin\makeSearchInput
makeSearchInput( $attrs=[])
Definition: Skin.php:2341
BaseTemplate\getPoweredByHTML
static getPoweredByHTML(Config $config)
Definition: BaseTemplate.php:68
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
SkinTemplate\$template
string $template
For QuickTemplate, the name of the subclass which will actually fill the template.
Definition: SkinTemplate.php:39
Skin\printSource
printSource()
Text with the permalink to the source page, usually shown on the footer of a printed page.
Definition: Skin.php:720
Skin\getSearchInputAttributes
getSearchInputAttributes( $attrs=[])
Definition: Skin.php:2320
SkinTemplate\outputPage
outputPage()
Initialize various variables and generate the template @stable to override.
Definition: SkinTemplate.php:132
Skin\buildNavUrls
buildNavUrls()
Build array of common navigation links.
Definition: Skin.php:1377
Skin\getAfterPortlet
getAfterPortlet(string $name)
Allows extensions to hook into known portlets and add stuff to them.
Definition: Skin.php:2403
SkinTemplate\makeTalkUrlDetails
makeTalkUrlDetails( $name, $urlaction='')
Definition: SkinTemplate.php:1068
SkinTemplate\buildPersonalPageItem
buildPersonalPageItem( $id='pt-userpage')
Build a personal page link.
Definition: SkinTemplate.php:980
Skin\logoText
logoText( $align='')
Definition: Skin.php:953
Linker\tooltip
static tooltip( $name, $options=null)
Returns raw bits of HTML, use titleAttrib()
Definition: Linker.php:2231
Skin\getNewtalks
getNewtalks()
Gets new talk page messages for the current user and returns an appropriate alert message (or an empt...
Definition: Skin.php:1699
SkinTemplate\runOnSkinTemplateNavigationHooks
runOnSkinTemplateNavigationHooks(SkinTemplate $skin, &$content_navigation)
Run hooks relating to navigation menu data.
Definition: SkinTemplate.php:1126
SkinTemplate\getNameSpaceKey
getNameSpaceKey()
Generate strings used for xml 'id' names.
Definition: SkinTemplate.php:1555
Title
Represents a title within MediaWiki.
Definition: Title.php:47
SkinTemplate\getFooterTemplateData
getFooterTemplateData()
Get rows that make up the footer.
Definition: SkinTemplate.php:792
Title\isSpecialPage
isSpecialPage()
Returns true if this is a special page.
Definition: Title.php:1267
SkinTemplate\buildLogoutLinkData
buildLogoutLinkData()
Build data required for "Logout" link.
Definition: SkinTemplate.php:939
LanguageCode\bcp47
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Definition: LanguageCode.php:175
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:138
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:1225
Skin\getFooterLinks
getFooterLinks()
Get template representation of the footer containing site footer links as well as standard footer lin...
Definition: Skin.php:2436
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:44
SkinTemplate\getWatchLinkAttrs
getWatchLinkAttrs(string $mode, Authority $performer, Title $title, ?string $action, bool $onPage)
Get the attributes for the watch link.
Definition: SkinTemplate.php:1090
Skin\initPage
initPage(OutputPage $out)
Definition: Skin.php:235
Skin\makeSearchButton
makeSearchButton( $mode, $attrs=[])
Definition: Skin.php:2352
SkinTemplate
Base class for QuickTemplate-based skins.
Definition: SkinTemplate.php:34
SkinTemplate\injectLegacyMenusIntoPersonalTools
injectLegacyMenusIntoPersonalTools(array $contentNavigation)
Insert legacy menu items from content navigation into the personal toolbar.
Definition: SkinTemplate.php:1567
SkinTemplate\generateHTML
generateHTML()
Subclasses not wishing to use the QuickTemplate render method can rewrite this method,...
Definition: SkinTemplate.php:122
BaseTemplate\getCopyrightIconHTML
static getCopyrightIconHTML(Config $config, Skin $skin)
Definition: BaseTemplate.php:41
SkinTemplate\getTemplateData
getTemplateData()
@inheritDoc
Definition: SkinTemplate.php:188
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
Definition: GlobalFunctions.php:330
Skin\makeFooterIcon
makeFooterIcon( $icon, $withImage='withImage')
Renders a $wgFooterIcons icon according to the method's arguments.
Definition: Skin.php:975
SkinTemplate\buildLoginData
buildLoginData( $returnto, $useCombinedLoginLink)
Build "Login" link.
Definition: SkinTemplate.php:628