1<?php 2/** 3 * This program is free software; you can redistribute it and/or modify 4 * it under the terms of the GNU General Public License as published by 5 * the Free Software Foundation; either version 2 of the License, or 6 * (at your option) any later version. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along 14 * with this program; if not, write to the Free Software Foundation, Inc., 15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 * http://www.gnu.org/copyleft/gpl.html 17 * 18 * @file 19 */ 20 21use MediaWiki\MediaWikiServices; 22 23/** 24 * Base class for template-based skins. 25 * 26 * Template-filler skin base class 27 * Formerly generic PHPTal (http://phptal.sourceforge.net/) skin 28 * Based on Brion's smarty skin 29 * @copyright Copyright © Gabriel Wicke -- http://www.aulinx.de/ 30 * 31 * @todo Needs some serious refactoring into functions that correspond 32 * to the computations individual esi snippets need. Most importantly no body 33 * parsing for most of those of course. 34 * 35 * @stable to extend 36 * 37 * @ingroup Skins 38 */ 39class SkinTemplate extends Skin { 40 /** 41 * @var string For QuickTemplate, the name of the subclass which will 42 * actually fill the template. 43 */ 44 public $template; 45 46 public $thispage; 47 public $titletxt; 48 public $userpage; 49 public $thisquery; 50 // TODO: Rename this to $isRegistered (but that's a breaking change) 51 public $loggedin; 52 public $username; 53 public $userpageUrlDetails; 54 55 /** 56 * Create the template engine object; we feed it a bunch of data 57 * and eventually it spits out some HTML. Should have interface 58 * roughly equivalent to PHPTAL 0.7. 59 * 60 * @param string $classname 61 * @return QuickTemplate 62 */ 63 protected function setupTemplate( $classname ) { 64 return new $classname( $this->getConfig() ); 65 } 66 67 /** 68 * @return QuickTemplate 69 */ 70 protected function setupTemplateForOutput() { 71 $this->setupTemplateContext(); 72 $template = $this->options['template'] ?? $this->template; 73 if ( !$template ) { 74 throw new RuntimeException( 75 'SkinTemplate skins must define a `template` either as a public' 76 . ' property of by passing in a`template` option to the constructor.' 77 ); 78 } 79 $tpl = $this->setupTemplate( $template ); 80 return $tpl; 81 } 82 83 /** 84 * Setup class properties that are necessary prior to calling 85 * setupTemplateForOutput. It must be called inside 86 * prepareQuickTemplate. 87 * This function may set local class properties that will be used 88 * by other methods, but should not make assumptions about the 89 * implementation of setupTemplateForOutput 90 * @since 1.35 91 */ 92 final protected function setupTemplateContext() { 93 $request = $this->getRequest(); 94 $user = $this->getUser(); 95 $title = $this->getTitle(); 96 97 $this->thispage = $title->getPrefixedDBkey(); 98 $this->titletxt = $title->getPrefixedText(); 99 $this->userpage = $user->getUserPage()->getPrefixedText(); 100 $query = []; 101 if ( !$request->wasPosted() ) { 102 $query = $request->getValues(); 103 unset( $query['title'] ); 104 unset( $query['returnto'] ); 105 unset( $query['returntoquery'] ); 106 } 107 $this->thisquery = wfArrayToCgi( $query ); 108 $this->loggedin = $user->isRegistered(); 109 $this->username = $user->getName(); 110 111 if ( $this->loggedin ) { 112 $this->userpageUrlDetails = self::makeUrlDetails( $this->userpage ); 113 } else { 114 # This won't be used in the standard skins, but we define it to preserve the interface 115 # To save time, we check for existence 116 $this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage ); 117 } 118 } 119 120 /** 121 * Subclasses not wishing to use the QuickTemplate 122 * render method can rewrite this method, for example to use 123 * TemplateParser::processTemplate 124 * @since 1.35 125 * @return string of complete document HTML to output to the page 126 * which includes `<!DOCTYPE>` and opening and closing html tags. 127 */ 128 public function generateHTML() { 129 $tpl = $this->prepareQuickTemplate(); 130 // execute template 131 return $tpl->execute(); 132 } 133 134 /** 135 * Initialize various variables and generate the template 136 * @stable to override 137 */ 138 public function outputPage() { 139 Profiler::instance()->setAllowOutput(); 140 $out = $this->getOutput(); 141 142 $this->initPage( $out ); 143 $out->addJsConfigVars( $this->getJsConfigVars() ); 144 145 // result may be an error 146 echo $this->generateHTML(); 147 } 148 149 /** 150 * Returns array of config variables that should be added only to this skin 151 * for use in JavaScript. 152 * Skins can override this to add variables to the page. 153 * @since 1.35 154 * @return array 155 */ 156 protected function getJsConfigVars() : array { 157 return []; 158 } 159 160 /** 161 * Wrap the body text with language information and identifiable element 162 * 163 * @param Title $title 164 * @param string $html body text 165 * @return string html 166 */ 167 protected function wrapHTML( $title, $html ) { 168 # An ID that includes the actual body text; without categories, contentSub, ... 169 $realBodyAttribs = [ 'id' => 'mw-content-text' ]; 170 171 # Add a mw-content-ltr/rtl class to be able to style based on text 172 # direction when the content is different from the UI language (only 173 # when viewing) 174 # Most information on special pages and file pages is in user language, 175 # rather than content language, so those will not get this 176 if ( Action::getActionName( $this ) === 'view' && 177 ( !$title->inNamespaces( NS_SPECIAL, NS_FILE ) || $title->isRedirect() ) ) { 178 $pageLang = $title->getPageViewLanguage(); 179 $realBodyAttribs['lang'] = $pageLang->getHtmlCode(); 180 $realBodyAttribs['dir'] = $pageLang->getDir(); 181 $realBodyAttribs['class'] = 'mw-content-' . $pageLang->getDir(); 182 } 183 184 return Html::rawElement( 'div', $realBodyAttribs, $html ); 185 } 186 187 /** 188 * Prepare user language attribute links 189 * @since 1.35 190 * @return string HTML attributes 191 */ 192 final protected function prepareUserLanguageAttributes() { 193 $userLang = $this->getLanguage(); 194 $userLangCode = $userLang->getHtmlCode(); 195 $userLangDir = $userLang->getDir(); 196 $contLang = MediaWikiServices::getInstance()->getContentLanguage(); 197 if ( 198 $userLangCode !== $contLang->getHtmlCode() || 199 $userLangDir !== $contLang->getDir() 200 ) { 201 $escUserlang = htmlspecialchars( $userLangCode ); 202 $escUserdir = htmlspecialchars( $userLangDir ); 203 // Attributes must be in double quotes because htmlspecialchars() doesn't 204 // escape single quotes 205 return " lang=\"$escUserlang\" dir=\"$escUserdir\""; 206 } 207 return ''; 208 } 209 210 /** 211 * Get template representation of the footer. 212 * @since 1.35 213 * @return array 214 */ 215 protected function getFooterIcons() { 216 $config = $this->getConfig(); 217 218 $footericons = []; 219 foreach ( $config->get( 'FooterIcons' ) as $footerIconsKey => &$footerIconsBlock ) { 220 if ( count( $footerIconsBlock ) > 0 ) { 221 $footericons[$footerIconsKey] = []; 222 foreach ( $footerIconsBlock as &$footerIcon ) { 223 if ( isset( $footerIcon['src'] ) ) { 224 if ( !isset( $footerIcon['width'] ) ) { 225 $footerIcon['width'] = 88; 226 } 227 if ( !isset( $footerIcon['height'] ) ) { 228 $footerIcon['height'] = 31; 229 } 230 } 231 232 // Only output icons which have an image. 233 // For historic reasons this mimics the `icononly` option 234 // for BaseTemplate::getFooterIcons. 235 // In some cases the icon may be an empty array. 236 // Filter these out. (See T269776) 237 if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) { 238 $footericons[$footerIconsKey][] = $footerIcon; 239 } 240 } 241 } 242 } 243 return $footericons; 244 } 245 246 /** 247 * Prepare undelete link for output in page. 248 * @since 1.35 249 * @return null|string HTML, or null if there is no undelete link. 250 */ 251 final protected function prepareUndeleteLink() { 252 $undelete = $this->getUndeleteLink(); 253 return $undelete === '' ? null : '<span class="subpages">' . $undelete . '</span>'; 254 } 255 256 /** 257 * initialize various variables and generate the template 258 * 259 * @since 1.23 260 * @return QuickTemplate The template to be executed by outputPage 261 */ 262 protected function prepareQuickTemplate() { 263 $title = $this->getTitle(); 264 $request = $this->getRequest(); 265 $out = $this->getOutput(); 266 $config = $this->getConfig(); 267 $tpl = $this->setupTemplateForOutput(); 268 269 $tpl->set( 'title', $out->getPageTitle() ); 270 $tpl->set( 'pagetitle', $out->getHTMLTitle() ); 271 $tpl->set( 'displaytitle', $out->mPageLinkTitle ); 272 273 $tpl->set( 'thispage', $this->thispage ); 274 $tpl->set( 'titleprefixeddbkey', $this->thispage ); 275 $tpl->set( 'titletext', $title->getText() ); 276 $tpl->set( 'articleid', $title->getArticleID() ); 277 278 $tpl->set( 'isarticle', $out->isArticle() ); 279 280 $tpl->set( 'subtitle', $this->prepareSubtitle() ); 281 $tpl->set( 'undelete', $this->prepareUndeleteLink() ); 282 283 $tpl->set( 'catlinks', $this->getCategories() ); 284 $feeds = $this->buildFeedUrls(); 285 $tpl->set( 'feeds', count( $feeds ) ? $feeds : false ); 286 287 $tpl->set( 'mimetype', $config->get( 'MimeType' ) ); 288 $tpl->set( 'charset', 'UTF-8' ); 289 $tpl->set( 'wgScript', $config->get( 'Script' ) ); 290 $tpl->set( 'skinname', $this->skinname ); 291 $tpl->set( 'skinclass', static::class ); 292 $tpl->set( 'skin', $this ); 293 $tpl->set( 'stylename', $this->stylename ); 294 $tpl->set( 'printable', $out->isPrintable() ); 295 $tpl->set( 'handheld', $request->getBool( 'handheld' ) ); 296 $tpl->set( 'loggedin', $this->loggedin ); 297 $tpl->set( 'notspecialpage', !$title->isSpecialPage() ); 298 299 // Deprecated since 1.36 300 $searchLink = $this->getSearchPageTitle()->getLocalURL(); 301 $tpl->set( 'searchaction', $searchLink ); 302 303 $tpl->set( 'searchtitle', $this->getSearchPageTitle()->getPrefixedDBkey() ); 304 $tpl->set( 'search', trim( $request->getVal( 'search' ) ) ); 305 $tpl->set( 'stylepath', $config->get( 'StylePath' ) ); 306 $tpl->set( 'articlepath', $config->get( 'ArticlePath' ) ); 307 $tpl->set( 'scriptpath', $config->get( 'ScriptPath' ) ); 308 $tpl->set( 'serverurl', $config->get( 'Server' ) ); 309 $logos = ResourceLoaderSkinModule::getAvailableLogos( $config ); 310 $tpl->set( 'logopath', $logos['1x'] ); 311 $tpl->set( 'sitename', $config->get( 'Sitename' ) ); 312 313 $userLang = $this->getLanguage(); 314 $userLangCode = $userLang->getHtmlCode(); 315 $userLangDir = $userLang->getDir(); 316 317 $tpl->set( 'lang', $userLangCode ); 318 $tpl->set( 'dir', $userLangDir ); 319 $tpl->set( 'rtl', $userLang->isRTL() ); 320 321 $tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' ); 322 $tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed 323 $tpl->set( 'username', $this->loggedin ? $this->username : null ); 324 $tpl->set( 'userpage', $this->userpage ); 325 $tpl->set( 'userpageurl', $this->userpageUrlDetails['href'] ); 326 $tpl->set( 'userlang', $userLangCode ); 327 328 // Users can have their language set differently than the 329 // content of the wiki. For these users, tell the web browser 330 // that interface elements are in a different language. 331 $tpl->set( 'userlangattributes', $this->prepareUserLanguageAttributes() ); 332 $tpl->set( 'specialpageattributes', '' ); # obsolete 333 // Used by VectorBeta to insert HTML before content but after the 334 // heading for the page title. Defaults to empty string. 335 $tpl->set( 'prebodyhtml', '' ); 336 337 $tpl->set( 'newtalk', $this->getNewtalks() ); 338 $tpl->set( 'logo', $this->logoText() ); 339 340 $footerData = $this->getFooterLinks(); 341 $tpl->set( 'copyright', $footerData['info']['copyright'] ?? false ); 342 // No longer used 343 $tpl->set( 'viewcount', false ); 344 $tpl->set( 'lastmod', $footerData['info']['lastmod'] ?? false ); 345 $tpl->set( 'credits', $footerData['info']['credits'] ?? false ); 346 $tpl->set( 'numberofwatchingusers', false ); 347 348 $tpl->set( 'copyrightico', $this->getCopyrightIcon() ); 349 $tpl->set( 'poweredbyico', $this->getPoweredBy() ); 350 351 $tpl->set( 'disclaimer', $footerData['places']['disclaimer'] ?? false ); 352 $tpl->set( 'privacy', $footerData['places']['privacy'] ?? false ); 353 $tpl->set( 'about', $footerData['places']['about'] ?? false ); 354 355 // Flatten for compat with the 'footerlinks' key in QuickTemplate-based skins. 356 $flattenedfooterlinks = []; 357 foreach ( $footerData as $category => $links ) { 358 $flattenedfooterlinks[$category] = array_keys( $links ); 359 foreach ( $links as $key => $value ) { 360 // For full support with BaseTemplate we also need to 361 // copy over the keys. 362 $tpl->set( $key, $value ); 363 } 364 } 365 $tpl->set( 'footerlinks', $flattenedfooterlinks ); 366 $tpl->set( 'footericons', $this->getFooterIcons() ); 367 368 $tpl->set( 'indicators', $out->getIndicators() ); 369 370 $tpl->set( 'sitenotice', $this->getSiteNotice() ); 371 $tpl->set( 'printfooter', $this->printSource() ); 372 // Wrap the bodyText with #mw-content-text element 373 $tpl->set( 'bodytext', $this->wrapHTML( $title, $out->getHTML() ) ); 374 375 $tpl->set( 'language_urls', $this->getLanguages() ?: false ); 376 377 $content_navigation = $this->buildContentNavigationUrls(); 378 # Personal toolbar 379 $tpl->set( 'personal_urls', $this->insertNotificationsIntoPersonalTools( $content_navigation ) ); 380 // user-menu and notifications are new content navigation entries and aren't expected 381 // to be part of content_navigation or content_actions. Adding them in there breaks skins 382 // that do not expect it. 383 unset( $content_navigation['user-menu'], $content_navigation['notifications'] ); 384 $content_actions = $this->buildContentActionUrls( $content_navigation ); 385 $tpl->set( 'content_navigation', $content_navigation ); 386 $tpl->set( 'content_actions', $content_actions ); 387 388 $tpl->set( 'sidebar', $this->buildSidebar() ); 389 $tpl->set( 'nav_urls', $this->buildNavUrls() ); 390 391 // Do this last in case hooks above add bottom scripts 392 $tpl->set( 'bottomscripts', $this->bottomScripts() ); 393 394 // Set the head scripts near the end, in case the above actions resulted in added scripts 395 $tpl->set( 'headelement', $out->headElement( $this ) ); 396 397 $tpl->set( 'debug', '' ); 398 $tpl->set( 'debughtml', MWDebug::getHTMLDebugLog() ); 399 $tpl->set( 'reporttime', wfReportTime( $out->getCSP()->getNonce() ) ); 400 401 // original version by hansm 402 // See T60137 for information on deprecation. 403 if ( !$this->getHookRunner()->onSkinTemplateOutputPageBeforeExec( $this, $tpl ) ) { 404 wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!" ); 405 } 406 407 // Set the bodytext to another key so that skins can just output it on its own 408 // and output printfooter and debughtml separately 409 $tpl->set( 'bodycontent', $tpl->data['bodytext'] ); 410 411 // Append printfooter and debughtml onto bodytext so that skins that 412 // were already using bodytext before they were split out don't suddenly 413 // start not outputting information. 414 $tpl->data['bodytext'] .= Html::rawElement( 415 'div', 416 [ 'class' => 'printfooter' ], 417 "\n{$tpl->data['printfooter']}" 418 ) . "\n"; 419 $tpl->data['bodytext'] .= $tpl->data['debughtml']; 420 421 // allow extensions adding stuff after the page content. 422 // See Skin::afterContentHook() for further documentation. 423 $tpl->set( 'dataAfterContent', $this->afterContentHook() ); 424 425 return $tpl; 426 } 427 428 /** 429 * Get the HTML for the p-personal list 430 * @deprecated since 1.35, use SkinTemplate::makePersonalToolsList() 431 * @return string 432 */ 433 public function getPersonalToolsList() { 434 return $this->makePersonalToolsList(); 435 } 436 437 /** 438 * Get the HTML for the personal tools list 439 * Please ensure setupTemplateContext is called before calling this method. 440 * 441 * @since 1.31 442 * 443 * @param array|null $personalTools 444 * @param array $options 445 * @return string 446 */ 447 public function makePersonalToolsList( $personalTools = null, $options = [] ) { 448 $this->setupTemplateContext(); 449 $html = ''; 450 451 if ( $personalTools === null ) { 452 $personalTools = $this->getPersonalToolsForMakeListItem( 453 $this->buildPersonalUrls() 454 ); 455 } 456 457 foreach ( $personalTools as $key => $item ) { 458 $html .= $this->makeListItem( $key, $item, $options ); 459 } 460 461 return $html; 462 } 463 464 /** 465 * Get personal tools for the user 466 * 467 * @since 1.31 468 * 469 * @return array Array of personal tools 470 */ 471 public function getStructuredPersonalTools() { 472 // buildPersonalUrls requires the template context. 473 $this->setupTemplateContext(); 474 return $this->getPersonalToolsForMakeListItem( 475 $this->buildPersonalUrls() 476 ); 477 } 478 479 /** 480 * build array of urls for personal toolbar 481 * Please ensure setupTemplateContext is called before calling 482 * this method. 483 * @param bool $includeNotifications Sinc 1.36, notifications are optional 484 * @return array 485 */ 486 protected function buildPersonalUrls( bool $includeNotifications = true ) { 487 $title = $this->getTitle(); 488 $request = $this->getRequest(); 489 $pageurl = $title->getLocalURL(); 490 $services = MediaWikiServices::getInstance(); 491 $authManager = $services->getAuthManager(); 492 $permissionManager = $services->getPermissionManager(); 493 494 /* set up the default links for the personal toolbar */ 495 $personal_urls = []; 496 497 # Due to T34276, if a user does not have read permissions, 498 # $this->getTitle() will just give Special:Badtitle, which is 499 # not especially useful as a returnto parameter. Use the title 500 # from the request instead, if there was one. 501 if ( $this->getAuthority()->isAllowed( 'read' ) ) { 502 $page = $title; 503 } else { 504 $page = Title::newFromText( $request->getVal( 'title', '' ) ); 505 } 506 $page = $request->getVal( 'returnto', $page ); 507 $returnto = []; 508 if ( strval( $page ) !== '' ) { 509 $returnto['returnto'] = $page; 510 $query = $request->getVal( 'returntoquery', $this->thisquery ); 511 $paramsArray = wfCgiToArray( $query ); 512 $query = wfArrayToCgi( $paramsArray ); 513 if ( $query != '' ) { 514 $returnto['returntoquery'] = $query; 515 } 516 } 517 518 if ( $this->loggedin ) { 519 $personal_urls['userpage'] = [ 520 'text' => $this->username, 521 'href' => &$this->userpageUrlDetails['href'], 522 'class' => $this->userpageUrlDetails['exists'] ? false : 'new', 523 'exists' => $this->userpageUrlDetails['exists'], 524 'active' => ( $this->userpageUrlDetails['href'] == $pageurl ), 525 'dir' => 'auto' 526 ]; 527 528 // Merge notifications into the personal menu for older skins. 529 if ( $includeNotifications ) { 530 $contentNavigation = $this->buildContentNavigationUrls(); 531 532 $personal_urls += $contentNavigation['notifications']; 533 } 534 535 $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage ); 536 $personal_urls['mytalk'] = [ 537 'text' => $this->msg( 'mytalk' )->text(), 538 'href' => &$usertalkUrlDetails['href'], 539 'class' => $usertalkUrlDetails['exists'] ? false : 'new', 540 'exists' => $usertalkUrlDetails['exists'], 541 'active' => ( $usertalkUrlDetails['href'] == $pageurl ) 542 ]; 543 $href = self::makeSpecialUrl( 'Preferences' ); 544 $personal_urls['preferences'] = [ 545 'text' => $this->msg( 'mypreferences' )->text(), 546 'href' => $href, 547 'active' => ( $href == $pageurl ) 548 ]; 549 550 if ( $this->getAuthority()->isAllowed( 'viewmywatchlist' ) ) { 551 $href = self::makeSpecialUrl( 'Watchlist' ); 552 $personal_urls['watchlist'] = [ 553 'text' => $this->msg( 'mywatchlist' )->text(), 554 'href' => $href, 555 'active' => ( $href == $pageurl ) 556 ]; 557 } 558 559 # We need to do an explicit check for Special:Contributions, as we 560 # have to match both the title, and the target, which could come 561 # from request values (Special:Contributions?target=Jimbo_Wales) 562 # or be specified in "sub page" form 563 # (Special:Contributions/Jimbo_Wales). The plot 564 # thickens, because the Title object is altered for special pages, 565 # so it doesn't contain the original alias-with-subpage. 566 $origTitle = Title::newFromText( $request->getText( 'title' ) ); 567 if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) { 568 list( $spName, $spPar ) = 569 MediaWikiServices::getInstance()->getSpecialPageFactory()-> 570 resolveAlias( $origTitle->getText() ); 571 $active = $spName == 'Contributions' 572 && ( ( $spPar && $spPar == $this->username ) 573 || $request->getText( 'target' ) == $this->username ); 574 } else { 575 $active = false; 576 } 577 578 $href = self::makeSpecialUrlSubpage( 'Contributions', $this->username ); 579 $personal_urls['mycontris'] = [ 580 'text' => $this->msg( 'mycontris' )->text(), 581 'href' => $href, 582 'active' => $active 583 ]; 584 585 // if we can't set the user, we can't unset it either 586 if ( $request->getSession()->canSetUser() ) { 587 $personal_urls['logout'] = [ 588 'text' => $this->msg( 'pt-userlogout' )->text(), 589 'data-mw' => 'interface', 590 'href' => self::makeSpecialUrl( 'Userlogout', 591 // Note: userlogout link must always contain an & character, otherwise we might not be able 592 // to detect a buggy precaching proxy (T19790) 593 ( $title->isSpecial( 'Preferences' ) ? [] : $returnto ) ), 594 'active' => false 595 ]; 596 } 597 } else { 598 $useCombinedLoginLink = $this->getConfig()->get( 'UseCombinedLoginLink' ); 599 if ( !$authManager->canCreateAccounts() || !$authManager->canAuthenticateNow() ) { 600 // don't show combined login/signup link if one of those is actually not available 601 $useCombinedLoginLink = false; 602 } 603 604 $loginlink = $this->getAuthority()->isAllowed( 'createaccount' ) 605 && $useCombinedLoginLink ? 'nav-login-createaccount' : 'pt-login'; 606 607 $login_url = [ 608 'text' => $this->msg( $loginlink )->text(), 609 'href' => self::makeSpecialUrl( 'Userlogin', $returnto ), 610 'active' => $title->isSpecial( 'Userlogin' ) 611 || $title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink, 612 ]; 613 $createaccount_url = [ 614 'text' => $this->msg( 'pt-createaccount' )->text(), 615 'href' => self::makeSpecialUrl( 'CreateAccount', $returnto ), 616 'active' => $title->isSpecial( 'CreateAccount' ), 617 ]; 618 619 // No need to show Talk and Contributions to anons if they can't contribute! 620 // TODO: easy way to get anon authority! 621 if ( $permissionManager->groupHasPermission( '*', 'edit' ) ) { 622 // Non interactive placeholder for anonymous users. 623 // It's unstyled by default (black color). Skin that 624 // needs it, can style it using the 'pt-anonuserpage' id. 625 // Skin that does not need it should unset it. 626 $personal_urls['anonuserpage'] = [ 627 'text' => $this->msg( 'notloggedin' )->text(), 628 ]; 629 630 // Because of caching, we can't link directly to the IP talk and 631 // contributions pages. Instead we use the special page shortcuts 632 // (which work correctly regardless of caching). This means we can't 633 // determine whether these links are active or not, but since major 634 // skins (MonoBook, Vector) don't use this information, it's not a 635 // huge loss. 636 $personal_urls['anontalk'] = [ 637 'text' => $this->msg( 'anontalk' )->text(), 638 'href' => self::makeSpecialUrlSubpage( 'Mytalk', false ), 639 'active' => false 640 ]; 641 $personal_urls['anoncontribs'] = [ 642 'text' => $this->msg( 'anoncontribs' )->text(), 643 'href' => self::makeSpecialUrlSubpage( 'Mycontributions', false ), 644 'active' => false 645 ]; 646 } 647 648 if ( 649 $authManager->canCreateAccounts() 650 && $this->getAuthority()->isAllowed( 'createaccount' ) 651 && !$useCombinedLoginLink 652 ) { 653 $personal_urls['createaccount'] = $createaccount_url; 654 } 655 656 if ( $authManager->canAuthenticateNow() ) { 657 // TODO: easy way to get anon authority 658 $key = $permissionManager->groupHasPermission( '*', 'read' ) 659 ? 'login' 660 : 'login-private'; 661 $personal_urls[$key] = $login_url; 662 } 663 } 664 665 $this->getHookRunner()->onPersonalUrls( $personal_urls, $title, $this ); 666 667 return $personal_urls; 668 } 669 670 /** 671 * Builds an array with tab definition 672 * 673 * @param Title $title Page Where the tab links to 674 * @param string|string[]|MessageSpecifier $message Message or an array of message keys 675 * (will fall back) 676 * @param bool $selected Display the tab as selected 677 * @param string $query Query string attached to tab URL 678 * @param bool $checkEdit Check if $title exists and mark with .new if one doesn't 679 * 680 * @return array 681 * @param-taint $message tainted 682 */ 683 public function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) { 684 $classes = []; 685 if ( $selected ) { 686 $classes[] = 'selected'; 687 } 688 $exists = true; 689 if ( $checkEdit && !$title->isKnown() ) { 690 $classes[] = 'new'; 691 $exists = false; 692 if ( $query !== '' ) { 693 $query = 'action=edit&redlink=1&' . $query; 694 } else { 695 $query = 'action=edit&redlink=1'; 696 } 697 } 698 699 $services = MediaWikiServices::getInstance(); 700 $linkClass = $services->getLinkRenderer()->getLinkClasses( $title ); 701 702 if ( $message instanceof MessageSpecifier ) { 703 $msg = new Message( $message ); 704 $message = $message->getKey(); 705 } else { 706 // wfMessageFallback will nicely accept $message as an array of fallbacks 707 // or just a single key 708 $msg = wfMessageFallback( $message ); 709 if ( is_array( $message ) ) { 710 // for hook compatibility just keep the last message name 711 $message = end( $message ); 712 } 713 } 714 $msg->setContext( $this->getContext() ); 715 if ( $msg->exists() ) { 716 $text = $msg->text(); 717 } else { 718 $text = $services->getLanguageConverterFactory() 719 ->getLanguageConverter( $services->getContentLanguage() ) 720 ->convertNamespace( 721 $services->getNamespaceInfo() 722 ->getSubject( $title->getNamespace() ) 723 ); 724 } 725 726 $result = []; 727 if ( !$this->getHookRunner()->onSkinTemplateTabAction( $this, $title, $message, 728 $selected, $checkEdit, $classes, $query, $text, $result ) 729 ) { 730 return $result; 731 } 732 733 $result = [ 734 'class' => implode( ' ', $classes ), 735 'text' => $text, 736 'href' => $title->getLocalURL( $query ), 737 'exists' => $exists, 738 'primary' => true ]; 739 if ( $linkClass !== '' ) { 740 $result['link-class'] = $linkClass; 741 } 742 743 return $result; 744 } 745 746 /** 747 * @param string $name 748 * @param string|array $urlaction 749 * @return array 750 */ 751 private function makeTalkUrlDetails( $name, $urlaction = '' ) { 752 $title = Title::newFromText( $name ); 753 if ( !is_object( $title ) ) { 754 throw new MWException( __METHOD__ . " given invalid pagename $name" ); 755 } 756 $title = $title->getTalkPage(); 757 self::checkTitle( $title, $name ); 758 return [ 759 'href' => $title->getLocalURL( $urlaction ), 760 'exists' => $title->isKnown(), 761 ]; 762 } 763 764 /** 765 * @deprecated since 1.35, no longer used 766 * @param string $name 767 * @param string|array $urlaction 768 * @return array 769 */ 770 public function makeArticleUrlDetails( $name, $urlaction = '' ) { 771 wfDeprecated( __METHOD__, '1.35' ); 772 $title = Title::newFromText( $name ); 773 $title = $title->getSubjectPage(); 774 self::checkTitle( $title, $name ); 775 return [ 776 'href' => $title->getLocalURL( $urlaction ), 777 'exists' => $title->exists(), 778 ]; 779 } 780 781 /** 782 * Get the attributes for the watch link. 783 * @param string $mode Either 'watch' or 'unwatch' 784 * @param User $user 785 * @param Title $title 786 * @param string|null $action 787 * @param bool $onPage 788 * @return array 789 */ 790 private function getWatchLinkAttrs( 791 string $mode, User $user, Title $title, ?string $action, bool $onPage 792 ): array { 793 $class = 'mw-watchlink ' . ( 794 $onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : '' 795 ); 796 797 // Add class identifying the page is temporarily watched, if applicable. 798 if ( $this->getConfig()->get( 'WatchlistExpiry' ) && 799 $user->isTempWatched( $title ) 800 ) { 801 $class .= ' mw-watchlink-temp'; 802 } 803 804 return [ 805 'class' => $class, 806 // uses 'watch' or 'unwatch' message 807 'text' => $this->msg( $mode )->text(), 808 'href' => $title->getLocalURL( [ 'action' => $mode ] ), 809 // Set a data-mw=interface attribute, which the mediawiki.page.ajax 810 // module will look for to make sure it's a trusted link 811 'data' => [ 812 'mw' => 'interface', 813 ], 814 ]; 815 } 816 817 /** 818 * a structured array of links usually used for the tabs in a skin 819 * 820 * There are 4 standard sections 821 * namespaces: Used for namespace tabs like special, page, and talk namespaces 822 * views: Used for primary page views like read, edit, history 823 * actions: Used for most extra page actions like deletion, protection, etc... 824 * variants: Used to list the language variants for the page 825 * 826 * Each section's value is a key/value array of links for that section. 827 * The links themselves have these common keys: 828 * - class: The css classes to apply to the tab 829 * - text: The text to display on the tab 830 * - href: The href for the tab to point to 831 * - rel: An optional rel= for the tab's link 832 * - redundant: If true the tab will be dropped in skins using content_actions 833 * this is useful for tabs like "Read" which only have meaning in skins that 834 * take special meaning from the grouped structure of content_navigation 835 * 836 * Views also have an extra key which can be used: 837 * - primary: If this is not true skins like vector may try to hide the tab 838 * when the user has limited space in their browser window 839 * 840 * content_navigation using code also expects these ids to be present on the 841 * links, however these are usually automatically generated by SkinTemplate 842 * itself and are not necessary when using a hook. The only things these may 843 * matter to are people modifying content_navigation after it's initial creation: 844 * - id: A "preferred" id, most skins are best off outputting this preferred 845 * id for best compatibility. 846 * - tooltiponly: This is set to true for some tabs in cases where the system 847 * believes that the accesskey should not be added to the tab. 848 * 849 * @return array 850 */ 851 protected function buildContentNavigationUrls() { 852 // Display tabs for the relevant title rather than always the title itself 853 $title = $this->getRelevantTitle(); 854 $onPage = $title->equals( $this->getTitle() ); 855 856 $out = $this->getOutput(); 857 $request = $this->getRequest(); 858 $user = $this->getUser(); 859 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); 860 861 $content_navigation = [ 862 'user-menu' => $this->buildPersonalUrls( false ), 863 'notifications' => [], 864 'namespaces' => [], 865 'views' => [], 866 'actions' => [], 867 'variants' => [] 868 ]; 869 870 // parameters 871 $action = $request->getVal( 'action', 'view' ); 872 873 $userCanRead = $this->getAuthority()->probablyCan( 'read', $title ); 874 875 $preventActiveTabs = false; 876 $this->getHookRunner()->onSkinTemplatePreventOtherActiveTabs( $this, $preventActiveTabs ); 877 878 // Checks if page is some kind of content 879 if ( $title->canExist() ) { 880 // Gets page objects for the related namespaces 881 $subjectPage = $title->getSubjectPage(); 882 $talkPage = $title->getTalkPage(); 883 884 // Determines if this is a talk page 885 $isTalk = $title->isTalkPage(); 886 887 // Generates XML IDs from namespace names 888 $subjectId = $title->getNamespaceKey( '' ); 889 890 if ( $subjectId == 'main' ) { 891 $talkId = 'talk'; 892 } else { 893 $talkId = "{$subjectId}_talk"; 894 } 895 896 $skname = $this->skinname; 897 898 // Adds namespace links 899 if ( $subjectId === 'user' ) { 900 $subjectMsg = wfMessage( 'nstab-user', $subjectPage->getRootText() ); 901 } else { 902 $subjectMsg = [ "nstab-$subjectId" ]; 903 } 904 if ( $subjectPage->isMainPage() ) { 905 array_unshift( $subjectMsg, 'mainpage-nstab' ); 906 } 907 908 $content_navigation['namespaces'][$subjectId] = $this->tabAction( 909 $subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs, '', $userCanRead 910 ); 911 $content_navigation['namespaces'][$subjectId]['context'] = 'subject'; 912 $content_navigation['namespaces'][$talkId] = $this->tabAction( 913 $talkPage, [ "nstab-$talkId", 'talk' ], $isTalk && !$preventActiveTabs, '', $userCanRead 914 ); 915 $content_navigation['namespaces'][$talkId]['context'] = 'talk'; 916 917 if ( $userCanRead ) { 918 // Adds "view" view link 919 if ( $title->isKnown() ) { 920 $content_navigation['views']['view'] = $this->tabAction( 921 $isTalk ? $talkPage : $subjectPage, 922 [ "$skname-view-view", 'view' ], 923 ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true 924 ); 925 // signal to hide this from simple content_actions 926 $content_navigation['views']['view']['redundant'] = true; 927 } 928 929 $page = $this->canUseWikiPage() ? $this->getWikiPage() : false; 930 $isRemoteContent = $page && !$page->isLocal(); 931 932 // If it is a non-local file, show a link to the file in its own repository 933 // @todo abstract this for remote content that isn't a file 934 if ( $isRemoteContent ) { 935 $content_navigation['views']['view-foreign'] = [ 936 'class' => '', 937 'text' => wfMessageFallback( "$skname-view-foreign", 'view-foreign' )-> 938 setContext( $this->getContext() )-> 939 params( $page->getWikiDisplayName() )->text(), 940 'href' => $page->getSourceURL(), 941 'primary' => false, 942 ]; 943 } 944 945 // Checks if user can edit the current page if it exists or create it otherwise 946 if ( $this->getAuthority()->probablyCan( 'edit', $title ) && 947 ( $title->exists() || 948 $this->getAuthority()->probablyCan( 'create', $title ) ) 949 ) { 950 // Builds CSS class for talk page links 951 $isTalkClass = $isTalk ? ' istalk' : ''; 952 // Whether the user is editing the page 953 $isEditing = $onPage && ( $action == 'edit' || $action == 'submit' ); 954 // Whether to show the "Add a new section" tab 955 // Checks if this is a current rev of talk page and is not forced to be hidden 956 $showNewSection = !$out->forceHideNewSectionLink() 957 && ( ( $isTalk && $out->isRevisionCurrent() ) || $out->showNewSectionLink() ); 958 $section = $request->getVal( 'section' ); 959 960 if ( $title->exists() 961 || ( $title->inNamespace( NS_MEDIAWIKI ) 962 && $title->getDefaultMessageText() !== false 963 ) 964 ) { 965 $msgKey = $isRemoteContent ? 'edit-local' : 'edit'; 966 } else { 967 $msgKey = $isRemoteContent ? 'create-local' : 'create'; 968 } 969 $content_navigation['views']['edit'] = [ 970 'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection ) 971 ? 'selected' 972 : '' 973 ) . $isTalkClass, 974 'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey ) 975 ->setContext( $this->getContext() )->text(), 976 'href' => $title->getLocalURL( $this->editUrlOptions() ), 977 'primary' => !$isRemoteContent, // don't collapse this in vector 978 ]; 979 980 // section link 981 if ( $showNewSection ) { 982 // Adds new section link 983 // $content_navigation['actions']['addsection'] 984 $content_navigation['views']['addsection'] = [ 985 'class' => ( $isEditing && $section == 'new' ) ? 'selected' : false, 986 'text' => wfMessageFallback( "$skname-action-addsection", 'addsection' ) 987 ->setContext( $this->getContext() )->text(), 988 'href' => $title->getLocalURL( 'action=edit§ion=new' ) 989 ]; 990 } 991 // Checks if the page has some kind of viewable source content 992 } elseif ( $title->hasSourceText() ) { 993 // Adds view source view link 994 $content_navigation['views']['viewsource'] = [ 995 'class' => ( $onPage && $action == 'edit' ) ? 'selected' : false, 996 'text' => wfMessageFallback( "$skname-action-viewsource", 'viewsource' ) 997 ->setContext( $this->getContext() )->text(), 998 'href' => $title->getLocalURL( $this->editUrlOptions() ), 999 'primary' => true, // don't collapse this in vector 1000 ]; 1001 } 1002 1003 // Checks if the page exists 1004 if ( $title->exists() ) { 1005 // Adds history view link 1006 $content_navigation['views']['history'] = [ 1007 'class' => ( $onPage && $action == 'history' ) ? 'selected' : false, 1008 'text' => wfMessageFallback( "$skname-view-history", 'history_short' ) 1009 ->setContext( $this->getContext() )->text(), 1010 'href' => $title->getLocalURL( 'action=history' ), 1011 ]; 1012 1013 if ( $this->getAuthority()->probablyCan( 'delete', $title ) ) { 1014 $content_navigation['actions']['delete'] = [ 1015 'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false, 1016 'text' => wfMessageFallback( "$skname-action-delete", 'delete' ) 1017 ->setContext( $this->getContext() )->text(), 1018 'href' => $title->getLocalURL( 'action=delete' ) 1019 ]; 1020 } 1021 1022 if ( $this->getAuthority()->probablyCan( 'move', $title ) ) { 1023 $moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() ); 1024 $content_navigation['actions']['move'] = [ 1025 'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false, 1026 'text' => wfMessageFallback( "$skname-action-move", 'move' ) 1027 ->setContext( $this->getContext() )->text(), 1028 'href' => $moveTitle->getLocalURL() 1029 ]; 1030 } 1031 } else { 1032 // article doesn't exist or is deleted 1033 if ( $this->getAuthority()->probablyCan( 'deletedhistory', $title ) ) { 1034 $n = $title->getDeletedEditsCount(); 1035 if ( $n ) { 1036 $undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ); 1037 // If the user can't undelete but can view deleted 1038 // history show them a "View .. deleted" tab instead. 1039 $msgKey = $this->getAuthority()->probablyCan( 'undelete', $title ) ? 1040 'undelete' : 'viewdeleted'; 1041 $content_navigation['actions']['undelete'] = [ 1042 'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false, 1043 'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" ) 1044 ->setContext( $this->getContext() )->numParams( $n )->text(), 1045 'href' => $undelTitle->getLocalURL() 1046 ]; 1047 } 1048 } 1049 } 1050 1051 if ( $this->getAuthority()->probablyCan( 'protect', $title ) && 1052 $title->getRestrictionTypes() && 1053 $permissionManager->getNamespaceRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ] 1054 ) { 1055 $mode = $title->isProtected() ? 'unprotect' : 'protect'; 1056 $content_navigation['actions'][$mode] = [ 1057 'class' => ( $onPage && $action == $mode ) ? 'selected' : false, 1058 'text' => wfMessageFallback( "$skname-action-$mode", $mode ) 1059 ->setContext( $this->getContext() )->text(), 1060 'href' => $title->getLocalURL( "action=$mode" ) 1061 ]; 1062 } 1063 1064 // Checks if the user is logged in 1065 if ( $this->loggedin && $this->getAuthority() 1066 ->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) 1067 ) { 1068 /** 1069 * The following actions use messages which, if made particular to 1070 * the any specific skins, would break the Ajax code which makes this 1071 * action happen entirely inline. OutputPage::getJSVars 1072 * defines a set of messages in a javascript object - and these 1073 * messages are assumed to be global for all skins. Without making 1074 * a change to that procedure these messages will have to remain as 1075 * the global versions. 1076 */ 1077 $mode = $user->isWatched( $title ) ? 'unwatch' : 'watch'; 1078 1079 // Add the watch/unwatch link. 1080 $content_navigation['actions'][$mode] = $this->getWatchLinkAttrs( 1081 $mode, 1082 $user, 1083 $title, 1084 $action, 1085 $onPage 1086 ); 1087 } 1088 } 1089 1090 $this->getHookRunner()->onSkinTemplateNavigation( $this, $content_navigation ); 1091 1092 $languageConverterFactory = MediaWikiServices::getInstance()->getLanguageConverterFactory(); 1093 1094 if ( $userCanRead && !$languageConverterFactory->isConversionDisabled() ) { 1095 $pageLang = $title->getPageLanguage(); 1096 $converter = $languageConverterFactory 1097 ->getLanguageConverter( $pageLang ); 1098 // Checks that language conversion is enabled and variants exist 1099 // And if it is not in the special namespace 1100 if ( $converter->hasVariants() ) { 1101 // Gets list of language variants 1102 $variants = $converter->getVariants(); 1103 // Gets preferred variant (note that user preference is 1104 // only possible for wiki content language variant) 1105 $preferred = $converter->getPreferredVariant(); 1106 if ( Action::getActionName( $this ) === 'view' ) { 1107 $params = $request->getQueryValues(); 1108 unset( $params['title'] ); 1109 } else { 1110 $params = []; 1111 } 1112 // Loops over each variant 1113 foreach ( $variants as $code ) { 1114 // Gets variant name from language code 1115 $varname = $pageLang->getVariantname( $code ); 1116 // Appends variant link 1117 $content_navigation['variants'][] = [ 1118 'class' => ( $code == $preferred ) ? 'selected' : false, 1119 'text' => $varname, 1120 'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ), 1121 'lang' => LanguageCode::bcp47( $code ), 1122 'hreflang' => LanguageCode::bcp47( $code ), 1123 ]; 1124 } 1125 } 1126 } 1127 } else { 1128 // If it's not content, and a request URL is set it's got to be a special page 1129 try { 1130 $url = $request->getRequestURL(); 1131 } catch ( MWException $e ) { 1132 $url = false; 1133 } 1134 $content_navigation['namespaces']['special'] = [ 1135 'class' => 'selected', 1136 'text' => $this->msg( 'nstab-special' )->text(), 1137 'href' => $url, // @see: T4457, T4510 1138 'context' => 'subject' 1139 ]; 1140 $this->getHookRunner()->onSkinTemplateNavigation__SpecialPage( 1141 $this, $content_navigation ); 1142 } 1143 1144 // Equiv to SkinTemplateContentActions 1145 $this->getHookRunner()->onSkinTemplateNavigation__Universal( 1146 $this, $content_navigation ); 1147 1148 // Setup xml ids and tooltip info 1149 foreach ( $content_navigation as $section => &$links ) { 1150 foreach ( $links as $key => &$link ) { 1151 // Allow links to set their own id for backwards compatibility reasons. 1152 if ( isset( $link['id'] ) ) { 1153 continue; 1154 } 1155 $xmlID = $key; 1156 if ( isset( $link['context'] ) && $link['context'] == 'subject' ) { 1157 $xmlID = 'ca-nstab-' . $xmlID; 1158 } elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) { 1159 $xmlID = 'ca-talk'; 1160 $link['rel'] = 'discussion'; 1161 } elseif ( $section == 'variants' ) { 1162 $xmlID = 'ca-varlang-' . $xmlID; 1163 } else { 1164 $xmlID = 'ca-' . $xmlID; 1165 } 1166 $link['id'] = $xmlID; 1167 } 1168 } 1169 1170 # We don't want to give the watch tab an accesskey if the 1171 # page is being edited, because that conflicts with the 1172 # accesskey on the watch checkbox. We also don't want to 1173 # give the edit tab an accesskey, because that's fairly 1174 # superfluous and conflicts with an accesskey (Ctrl-E) often 1175 # used for editing in Safari. 1176 if ( in_array( $action, [ 'edit', 'submit' ] ) ) { 1177 if ( isset( $content_navigation['views']['edit'] ) ) { 1178 $content_navigation['views']['edit']['tooltiponly'] = true; 1179 } 1180 if ( isset( $content_navigation['actions']['watch'] ) ) { 1181 $content_navigation['actions']['watch']['tooltiponly'] = true; 1182 } 1183 if ( isset( $content_navigation['actions']['unwatch'] ) ) { 1184 $content_navigation['actions']['unwatch']['tooltiponly'] = true; 1185 } 1186 } 1187 1188 return $content_navigation; 1189 } 1190 1191 /** 1192 * an array of edit links by default used for the tabs 1193 * @param array $content_navigation 1194 * @return array 1195 */ 1196 private function buildContentActionUrls( $content_navigation ) { 1197 // content_actions has been replaced with content_navigation for backwards 1198 // compatibility and also for skins that just want simple tabs content_actions 1199 // is now built by flattening the content_navigation arrays into one 1200 1201 $content_actions = []; 1202 1203 foreach ( $content_navigation as $navigation => $links ) { 1204 foreach ( $links as $key => $value ) { 1205 if ( isset( $value['redundant'] ) && $value['redundant'] ) { 1206 // Redundant tabs are dropped from content_actions 1207 continue; 1208 } 1209 1210 // content_actions used to have ids built using the "ca-$key" pattern 1211 // so the xmlID based id is much closer to the actual $key that we want 1212 // for that reason we'll just strip out the ca- if present and use 1213 // the latter potion of the "id" as the $key 1214 if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) { 1215 $key = substr( $value['id'], 3 ); 1216 } 1217 1218 if ( isset( $content_actions[$key] ) ) { 1219 wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " . 1220 "content_navigation into content_actions." ); 1221 continue; 1222 } 1223 1224 $content_actions[$key] = $value; 1225 } 1226 } 1227 1228 return $content_actions; 1229 } 1230 1231 /** 1232 * build array of common navigation links and run 1233 * the SkinTemplateBuildNavUrlsNav_urlsAfterPermalink hook. 1234 * @inheritDoc 1235 * @return array 1236 */ 1237 protected function buildNavUrls() { 1238 $navUrls = parent::buildNavUrls(); 1239 $out = $this->getOutput(); 1240 if ( !$out->isArticle() ) { 1241 return $navUrls; 1242 } 1243 $modifiedNavUrls = []; 1244 foreach ( $navUrls as $key => $url ) { 1245 $modifiedNavUrls[$key] = $url; 1246 if ( $key === 'permalink' ) { 1247 $revid = $out->getRevisionId(); 1248 // Use the copy of revision ID in case this undocumented, 1249 // shady hook tries to mess with internals. 1250 $this->getHookRunner()->onSkinTemplateBuildNavUrlsNav_urlsAfterPermalink( 1251 $this, $modifiedNavUrls, $revid, $revid 1252 ); 1253 } 1254 } 1255 return $modifiedNavUrls; 1256 } 1257 1258 /** 1259 * Generate strings used for xml 'id' names 1260 * @deprecated since 1.35, use Title::getNamespaceKey() instead 1261 * @return string 1262 */ 1263 protected function getNameSpaceKey() { 1264 return $this->getTitle()->getNamespaceKey(); 1265 } 1266 1267 /** 1268 * Insert the notifications content navigation into the personal tools, in their old position, 1269 * following the userpage. 1270 * 1271 * @internal 1272 * 1273 * @param array $contentNavigation 1274 * @return array 1275 */ 1276 final protected function insertNotificationsIntoPersonalTools( 1277 array $contentNavigation 1278 ) : array { 1279 // userpage is only defined for logged-in users, and wfArrayInsertAfter requires the 1280 // $after parameter to be a known key in the array. 1281 if ( isset( $contentNavigation['user-menu']['userpage'] ) ) { 1282 return wfArrayInsertAfter( 1283 $contentNavigation['user-menu'], 1284 $contentNavigation['notifications'], 1285 'userpage' 1286 ); 1287 } else { 1288 return $contentNavigation['user-menu']; 1289 } 1290 } 1291} 1292