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&section=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