Completed
Branch master (939199)
by
unknown
39:35
created

includes/skins/SkinTemplate.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
21
use MediaWiki\Auth\AuthManager;
22
use MediaWiki\MediaWikiServices;
23
24
/**
25
 * Base class for template-based skins.
26
 *
27
 * Template-filler skin base class
28
 * Formerly generic PHPTal (http://phptal.sourceforge.net/) skin
29
 * Based on Brion's smarty skin
30
 * @copyright Copyright © Gabriel Wicke -- http://www.aulinx.de/
31
 *
32
 * @todo Needs some serious refactoring into functions that correspond
33
 * to the computations individual esi snippets need. Most importantly no body
34
 * parsing for most of those of course.
35
 *
36
 * @ingroup Skins
37
 */
38
class SkinTemplate extends Skin {
39
	/**
40
	 * @var string Name of our skin, it probably needs to be all lower case.
41
	 *   Child classes should override the default.
42
	 */
43
	public $skinname = 'monobook';
44
45
	/**
46
	 * @var string For QuickTemplate, the name of the subclass which will
47
	 *   actually fill the template.  Child classes should override the default.
48
	 */
49
	public $template = 'QuickTemplate';
50
51
	public $thispage;
52
	public $titletxt;
53
	public $userpage;
54
	public $thisquery;
55
	public $loggedin;
56
	public $username;
57
	public $userpageUrlDetails;
58
59
	/**
60
	 * Add specific styles for this skin
61
	 *
62
	 * @param OutputPage $out
63
	 */
64
	function setupSkinUserCss( OutputPage $out ) {
65
		$moduleStyles = [
66
			'mediawiki.legacy.shared',
67
			'mediawiki.legacy.commonPrint',
68
			'mediawiki.sectionAnchor'
69
		];
70
		if ( $out->isSyndicated() ) {
71
			$moduleStyles[] = 'mediawiki.feedlink';
72
		}
73
74
		// Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
75
		// on every page is deprecated. Express a dependency instead.
76
		if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
77
			$moduleStyles[] = 'mediawiki.ui.button';
78
		}
79
80
		$out->addModuleStyles( $moduleStyles );
81
	}
82
83
	/**
84
	 * Create the template engine object; we feed it a bunch of data
85
	 * and eventually it spits out some HTML. Should have interface
86
	 * roughly equivalent to PHPTAL 0.7.
87
	 *
88
	 * @param string $classname
89
	 * @param bool|string $repository Subdirectory where we keep template files
90
	 * @param bool|string $cache_dir
91
	 * @return QuickTemplate
92
	 * @private
93
	 */
94
	function setupTemplate( $classname, $repository = false, $cache_dir = false ) {
95
		return new $classname( $this->getConfig() );
96
	}
97
98
	/**
99
	 * Generates array of language links for the current page
100
	 *
101
	 * @return array
102
	 */
103
	public function getLanguages() {
104
		global $wgHideInterlanguageLinks;
105
		if ( $wgHideInterlanguageLinks ) {
106
			return [];
107
		}
108
109
		$userLang = $this->getLanguage();
110
		$languageLinks = [];
111
112
		foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
113
			$class = 'interlanguage-link interwiki-' . explode( ':', $languageLinkText, 2 )[0];
114
115
			$languageLinkTitle = Title::newFromText( $languageLinkText );
116
			if ( $languageLinkTitle ) {
117
				$ilInterwikiCode = $languageLinkTitle->getInterwiki();
118
				$ilLangName = Language::fetchLanguageName( $ilInterwikiCode );
119
120
				if ( strval( $ilLangName ) === '' ) {
121
					$ilDisplayTextMsg = wfMessage( "interlanguage-link-$ilInterwikiCode" );
122
					if ( !$ilDisplayTextMsg->isDisabled() ) {
123
						// Use custom MW message for the display text
124
						$ilLangName = $ilDisplayTextMsg->text();
125
					} else {
126
						// Last resort: fallback to the language link target
127
						$ilLangName = $languageLinkText;
128
					}
129
				} else {
130
					// Use the language autonym as display text
131
					$ilLangName = $this->formatLanguageName( $ilLangName );
132
				}
133
134
				// CLDR extension or similar is required to localize the language name;
135
				// otherwise we'll end up with the autonym again.
136
				$ilLangLocalName = Language::fetchLanguageName(
137
					$ilInterwikiCode,
138
					$userLang->getCode()
139
				);
140
141
				$languageLinkTitleText = $languageLinkTitle->getText();
142
				if ( $ilLangLocalName === '' ) {
143
					$ilFriendlySiteName = wfMessage( "interlanguage-link-sitename-$ilInterwikiCode" );
144
					if ( !$ilFriendlySiteName->isDisabled() ) {
145
						if ( $languageLinkTitleText === '' ) {
146
							$ilTitle = wfMessage(
147
								'interlanguage-link-title-nonlangonly',
148
								$ilFriendlySiteName->text()
149
							)->text();
150
						} else {
151
							$ilTitle = wfMessage(
152
								'interlanguage-link-title-nonlang',
153
								$languageLinkTitleText,
154
								$ilFriendlySiteName->text()
155
							)->text();
156
						}
157
					} else {
158
						// we have nothing friendly to put in the title, so fall back to
159
						// displaying the interlanguage link itself in the title text
160
						// (similar to what is done in page content)
161
						$ilTitle = $languageLinkTitle->getInterwiki() .
162
							":$languageLinkTitleText";
163
					}
164
				} elseif ( $languageLinkTitleText === '' ) {
165
					$ilTitle = wfMessage(
166
						'interlanguage-link-title-langonly',
167
						$ilLangLocalName
168
					)->text();
169
				} else {
170
					$ilTitle = wfMessage(
171
						'interlanguage-link-title',
172
						$languageLinkTitleText,
173
						$ilLangLocalName
174
					)->text();
175
				}
176
177
				$ilInterwikiCodeBCP47 = wfBCP47( $ilInterwikiCode );
178
				$languageLink = [
179
					'href' => $languageLinkTitle->getFullURL(),
180
					'text' => $ilLangName,
181
					'title' => $ilTitle,
182
					'class' => $class,
183
					'link-class' => 'interlanguage-link-target',
184
					'lang' => $ilInterwikiCodeBCP47,
185
					'hreflang' => $ilInterwikiCodeBCP47,
186
				];
187
				Hooks::run(
188
					'SkinTemplateGetLanguageLink',
189
					[ &$languageLink, $languageLinkTitle, $this->getTitle(), $this->getOutput() ]
190
				);
191
				$languageLinks[] = $languageLink;
192
			}
193
		}
194
195
		return $languageLinks;
196
	}
197
198
	protected function setupTemplateForOutput() {
199
200
		$request = $this->getRequest();
201
		$user = $this->getUser();
202
		$title = $this->getTitle();
203
204
		$tpl = $this->setupTemplate( $this->template, 'skins' );
205
206
		$this->thispage = $title->getPrefixedDBkey();
207
		$this->titletxt = $title->getPrefixedText();
208
		$this->userpage = $user->getUserPage()->getPrefixedText();
209
		$query = [];
210 View Code Duplication
		if ( !$request->wasPosted() ) {
211
			$query = $request->getValues();
212
			unset( $query['title'] );
213
			unset( $query['returnto'] );
214
			unset( $query['returntoquery'] );
215
		}
216
		$this->thisquery = wfArrayToCgi( $query );
217
		$this->loggedin = $user->isLoggedIn();
218
		$this->username = $user->getName();
219
220
		if ( $this->loggedin ) {
221
			$this->userpageUrlDetails = self::makeUrlDetails( $this->userpage );
222
		} else {
223
			# This won't be used in the standard skins, but we define it to preserve the interface
224
			# To save time, we check for existence
225
			$this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage );
226
		}
227
228
		return $tpl;
229
	}
230
231
	/**
232
	 * initialize various variables and generate the template
233
	 *
234
	 * @param OutputPage $out
235
	 */
236
	function outputPage( OutputPage $out = null ) {
237
		Profiler::instance()->setTemplated( true );
238
239
		$oldContext = null;
240
		if ( $out !== null ) {
241
			// Deprecated since 1.20, note added in 1.25
242
			wfDeprecated( __METHOD__, '1.25' );
243
			$oldContext = $this->getContext();
244
			$this->setContext( $out->getContext() );
245
		}
246
247
		$out = $this->getOutput();
248
249
		$this->initPage( $out );
250
		$tpl = $this->prepareQuickTemplate();
251
		// execute template
252
		$res = $tpl->execute();
253
254
		// result may be an error
255
		$this->printOrError( $res );
256
257
		if ( $oldContext ) {
258
			$this->setContext( $oldContext );
259
		}
260
261
	}
262
263
	/**
264
	 * Wrap the body text with language information and identifiable element
265
	 *
266
	 * @param Title $title
267
	 * @param string $html body text
268
	 * @return string html
269
	 */
270
	protected function wrapHTML( $title, $html ) {
271
		# An ID that includes the actual body text; without categories, contentSub, ...
272
		$realBodyAttribs = [ 'id' => 'mw-content-text' ];
273
274
		# Add a mw-content-ltr/rtl class to be able to style based on text direction
275
		# when the content is different from the UI language, i.e.:
276
		# not for special pages or file pages AND only when viewing
277
		if ( !in_array( $title->getNamespace(), [ NS_SPECIAL, NS_FILE ] ) &&
278
			Action::getActionName( $this ) === 'view' ) {
279
			$pageLang = $title->getPageViewLanguage();
280
			$realBodyAttribs['lang'] = $pageLang->getHtmlCode();
281
			$realBodyAttribs['dir'] = $pageLang->getDir();
282
			$realBodyAttribs['class'] = 'mw-content-' . $pageLang->getDir();
283
		}
284
285
		return Html::rawElement( 'div', $realBodyAttribs, $html );
286
	}
287
288
	/**
289
	 * initialize various variables and generate the template
290
	 *
291
	 * @since 1.23
292
	 * @return QuickTemplate The template to be executed by outputPage
293
	 */
294
	protected function prepareQuickTemplate() {
295
		global $wgContLang, $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType,
296
			$wgSitename, $wgLogo, $wgMaxCredits,
297
			$wgShowCreditsIfMax, $wgArticlePath,
298
			$wgScriptPath, $wgServer;
299
300
		$title = $this->getTitle();
301
		$request = $this->getRequest();
302
		$out = $this->getOutput();
303
		$tpl = $this->setupTemplateForOutput();
304
305
		$tpl->set( 'title', $out->getPageTitle() );
306
		$tpl->set( 'pagetitle', $out->getHTMLTitle() );
307
		$tpl->set( 'displaytitle', $out->mPageLinkTitle );
308
309
		$tpl->setRef( 'thispage', $this->thispage );
310
		$tpl->setRef( 'titleprefixeddbkey', $this->thispage );
311
		$tpl->set( 'titletext', $title->getText() );
312
		$tpl->set( 'articleid', $title->getArticleID() );
313
314
		$tpl->set( 'isarticle', $out->isArticle() );
315
316
		$subpagestr = $this->subPageSubtitle();
317
		if ( $subpagestr !== '' ) {
318
			$subpagestr = '<span class="subpages">' . $subpagestr . '</span>';
319
		}
320
		$tpl->set( 'subtitle', $subpagestr . $out->getSubtitle() );
321
322
		$undelete = $this->getUndeleteLink();
323
		if ( $undelete === '' ) {
324
			$tpl->set( 'undelete', '' );
325
		} else {
326
			$tpl->set( 'undelete', '<span class="subpages">' . $undelete . '</span>' );
327
		}
328
329
		$tpl->set( 'catlinks', $this->getCategories() );
330
		if ( $out->isSyndicated() ) {
331
			$feeds = [];
332
			foreach ( $out->getSyndicationLinks() as $format => $link ) {
333
				$feeds[$format] = [
334
					// Messages: feed-atom, feed-rss
335
					'text' => $this->msg( "feed-$format" )->text(),
336
					'href' => $link
337
				];
338
			}
339
			$tpl->setRef( 'feeds', $feeds );
340
		} else {
341
			$tpl->set( 'feeds', false );
342
		}
343
344
		$tpl->setRef( 'mimetype', $wgMimeType );
345
		$tpl->setRef( 'jsmimetype', $wgJsMimeType );
346
		$tpl->set( 'charset', 'UTF-8' );
347
		$tpl->setRef( 'wgScript', $wgScript );
348
		$tpl->setRef( 'skinname', $this->skinname );
349
		$tpl->set( 'skinclass', get_class( $this ) );
350
		$tpl->setRef( 'skin', $this );
351
		$tpl->setRef( 'stylename', $this->stylename );
352
		$tpl->set( 'printable', $out->isPrintable() );
353
		$tpl->set( 'handheld', $request->getBool( 'handheld' ) );
354
		$tpl->setRef( 'loggedin', $this->loggedin );
355
		$tpl->set( 'notspecialpage', !$title->isSpecialPage() );
356
		$tpl->set( 'searchaction', $this->escapeSearchLink() );
357
		$tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey() );
358
		$tpl->set( 'search', trim( $request->getVal( 'search' ) ) );
359
		$tpl->setRef( 'stylepath', $wgStylePath );
360
		$tpl->setRef( 'articlepath', $wgArticlePath );
361
		$tpl->setRef( 'scriptpath', $wgScriptPath );
362
		$tpl->setRef( 'serverurl', $wgServer );
363
		$tpl->setRef( 'logopath', $wgLogo );
364
		$tpl->setRef( 'sitename', $wgSitename );
365
366
		$userLang = $this->getLanguage();
367
		$userLangCode = $userLang->getHtmlCode();
368
		$userLangDir = $userLang->getDir();
369
370
		$tpl->set( 'lang', $userLangCode );
371
		$tpl->set( 'dir', $userLangDir );
372
		$tpl->set( 'rtl', $userLang->isRTL() );
373
374
		$tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
375
		$tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed
376
		$tpl->set( 'username', $this->loggedin ? $this->username : null );
377
		$tpl->setRef( 'userpage', $this->userpage );
378
		$tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] );
379
		$tpl->set( 'userlang', $userLangCode );
380
381
		// Users can have their language set differently than the
382
		// content of the wiki. For these users, tell the web browser
383
		// that interface elements are in a different language.
384
		$tpl->set( 'userlangattributes', '' );
385
		$tpl->set( 'specialpageattributes', '' ); # obsolete
386
		// Used by VectorBeta to insert HTML before content but after the
387
		// heading for the page title. Defaults to empty string.
388
		$tpl->set( 'prebodyhtml', '' );
389
390
		if ( $userLangCode !== $wgContLang->getHtmlCode() || $userLangDir !== $wgContLang->getDir() ) {
391
			$escUserlang = htmlspecialchars( $userLangCode );
392
			$escUserdir = htmlspecialchars( $userLangDir );
393
			// Attributes must be in double quotes because htmlspecialchars() doesn't
394
			// escape single quotes
395
			$attrs = " lang=\"$escUserlang\" dir=\"$escUserdir\"";
396
			$tpl->set( 'userlangattributes', $attrs );
397
		}
398
399
		$tpl->set( 'newtalk', $this->getNewtalks() );
400
		$tpl->set( 'logo', $this->logoText() );
401
402
		$tpl->set( 'copyright', false );
403
		// No longer used
404
		$tpl->set( 'viewcount', false );
405
		$tpl->set( 'lastmod', false );
406
		$tpl->set( 'credits', false );
407
		$tpl->set( 'numberofwatchingusers', false );
408
		if ( $out->isArticle() && $title->exists() ) {
409
			if ( $this->isRevisionCurrent() ) {
410
				if ( $wgMaxCredits != 0 ) {
411
					$tpl->set( 'credits', Action::factory( 'credits', $this->getWikiPage(),
412
						$this->getContext() )->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) );
413
				} else {
414
					$tpl->set( 'lastmod', $this->lastModified() );
415
				}
416
			}
417
			$tpl->set( 'copyright', $this->getCopyright() );
418
		}
419
420
		$tpl->set( 'copyrightico', $this->getCopyrightIcon() );
421
		$tpl->set( 'poweredbyico', $this->getPoweredBy() );
422
		$tpl->set( 'disclaimer', $this->disclaimerLink() );
423
		$tpl->set( 'privacy', $this->privacyLink() );
424
		$tpl->set( 'about', $this->aboutLink() );
425
426
		$tpl->set( 'footerlinks', [
427
			'info' => [
428
				'lastmod',
429
				'numberofwatchingusers',
430
				'credits',
431
				'copyright',
432
			],
433
			'places' => [
434
				'privacy',
435
				'about',
436
				'disclaimer',
437
			],
438
		] );
439
440
		global $wgFooterIcons;
441
		$tpl->set( 'footericons', $wgFooterIcons );
442
		foreach ( $tpl->data['footericons'] as $footerIconsKey => &$footerIconsBlock ) {
443
			if ( count( $footerIconsBlock ) > 0 ) {
444
				foreach ( $footerIconsBlock as &$footerIcon ) {
445
					if ( isset( $footerIcon['src'] ) ) {
446
						if ( !isset( $footerIcon['width'] ) ) {
447
							$footerIcon['width'] = 88;
448
						}
449
						if ( !isset( $footerIcon['height'] ) ) {
450
							$footerIcon['height'] = 31;
451
						}
452
					}
453
				}
454
			} else {
455
				unset( $tpl->data['footericons'][$footerIconsKey] );
456
			}
457
		}
458
459
		$tpl->set( 'indicators', $out->getIndicators() );
460
461
		$tpl->set( 'sitenotice', $this->getSiteNotice() );
462
		$tpl->set( 'printfooter', $this->printSource() );
463
		// Wrap the bodyText with #mw-content-text element
464
		$out->mBodytext = $this->wrapHTML( $title, $out->mBodytext );
0 ignored issues
show
It seems like $title defined by $this->getTitle() on line 300 can be null; however, SkinTemplate::wrapHTML() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
465
		$tpl->setRef( 'bodytext', $out->mBodytext );
466
467
		$language_urls = $this->getLanguages();
468
		if ( count( $language_urls ) ) {
469
			$tpl->setRef( 'language_urls', $language_urls );
470
		} else {
471
			$tpl->set( 'language_urls', false );
472
		}
473
474
		# Personal toolbar
475
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
476
		$content_navigation = $this->buildContentNavigationUrls();
477
		$content_actions = $this->buildContentActionUrls( $content_navigation );
478
		$tpl->setRef( 'content_navigation', $content_navigation );
479
		$tpl->setRef( 'content_actions', $content_actions );
480
481
		$tpl->set( 'sidebar', $this->buildSidebar() );
482
		$tpl->set( 'nav_urls', $this->buildNavUrls() );
483
484
		// Do this last in case hooks above add bottom scripts
485
		$tpl->set( 'bottomscripts', $this->bottomScripts() );
486
487
		// Set the head scripts near the end, in case the above actions resulted in added scripts
488
		$tpl->set( 'headelement', $out->headElement( $this ) );
489
490
		$tpl->set( 'debug', '' );
491
		$tpl->set( 'debughtml', $this->generateDebugHTML() );
492
		$tpl->set( 'reporttime', wfReportTime() );
493
494
		// original version by hansm
495
		if ( !Hooks::run( 'SkinTemplateOutputPageBeforeExec', [ &$this, &$tpl ] ) ) {
496
			wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" );
497
		}
498
499
		// Set the bodytext to another key so that skins can just output it on its own
500
		// and output printfooter and debughtml separately
501
		$tpl->set( 'bodycontent', $tpl->data['bodytext'] );
502
503
		// Append printfooter and debughtml onto bodytext so that skins that
504
		// were already using bodytext before they were split out don't suddenly
505
		// start not outputting information.
506
		$tpl->data['bodytext'] .= Html::rawElement(
507
			'div',
508
			[ 'class' => 'printfooter' ],
509
			"\n{$tpl->data['printfooter']}"
510
		) . "\n";
511
		$tpl->data['bodytext'] .= $tpl->data['debughtml'];
512
513
		// allow extensions adding stuff after the page content.
514
		// See Skin::afterContentHook() for further documentation.
515
		$tpl->set( 'dataAfterContent', $this->afterContentHook() );
516
517
		return $tpl;
518
	}
519
520
	/**
521
	 * Get the HTML for the p-personal list
522
	 * @return string
523
	 */
524
	public function getPersonalToolsList() {
525
		$tpl = $this->setupTemplateForOutput();
526
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
527
		$html = '';
528
		foreach ( $tpl->getPersonalTools() as $key => $item ) {
529
			$html .= $tpl->makeListItem( $key, $item );
530
		}
531
		return $html;
532
	}
533
534
	/**
535
	 * Format language name for use in sidebar interlanguage links list.
536
	 * By default it is capitalized.
537
	 *
538
	 * @param string $name Language name, e.g. "English" or "español"
539
	 * @return string
540
	 * @private
541
	 */
542
	function formatLanguageName( $name ) {
543
		return $this->getLanguage()->ucfirst( $name );
544
	}
545
546
	/**
547
	 * Output the string, or print error message if it's
548
	 * an error object of the appropriate type.
549
	 * For the base class, assume strings all around.
550
	 *
551
	 * @param string $str
552
	 * @private
553
	 */
554
	function printOrError( $str ) {
555
		echo $str;
556
	}
557
558
	/**
559
	 * Output a boolean indicating if buildPersonalUrls should output separate
560
	 * login and create account links or output a combined link
561
	 * By default we simply return a global config setting that affects most skins
562
	 * This is setup as a method so that like with $wgLogo and getLogo() a skin
563
	 * can override this setting and always output one or the other if it has
564
	 * a reason it can't output one of the two modes.
565
	 * @return bool
566
	 */
567
	function useCombinedLoginLink() {
568
		global $wgUseCombinedLoginLink;
569
		return $wgUseCombinedLoginLink;
570
	}
571
572
	/**
573
	 * build array of urls for personal toolbar
574
	 * @return array
575
	 */
576
	protected function buildPersonalUrls() {
577
		$title = $this->getTitle();
578
		$request = $this->getRequest();
579
		$pageurl = $title->getLocalURL();
580
		$authManager = AuthManager::singleton();
581
582
		/* set up the default links for the personal toolbar */
583
		$personal_urls = [];
584
585
		# Due to bug 32276, if a user does not have read permissions,
586
		# $this->getTitle() will just give Special:Badtitle, which is
587
		# not especially useful as a returnto parameter. Use the title
588
		# from the request instead, if there was one.
589
		if ( $this->getUser()->isAllowed( 'read' ) ) {
590
			$page = $this->getTitle();
591
		} else {
592
			$page = Title::newFromText( $request->getVal( 'title', '' ) );
593
		}
594
		$page = $request->getVal( 'returnto', $page );
595
		$a = [];
596
		if ( strval( $page ) !== '' ) {
597
			$a['returnto'] = $page;
598
			$query = $request->getVal( 'returntoquery', $this->thisquery );
599
			if ( $query != '' ) {
600
				$a['returntoquery'] = $query;
601
			}
602
		}
603
604
		$returnto = wfArrayToCgi( $a );
605
		if ( $this->loggedin ) {
606
			$personal_urls['userpage'] = [
607
				'text' => $this->username,
608
				'href' => &$this->userpageUrlDetails['href'],
609
				'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
610
				'active' => ( $this->userpageUrlDetails['href'] == $pageurl ),
611
				'dir' => 'auto'
612
			];
613
			$usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage );
614
			$personal_urls['mytalk'] = [
615
				'text' => $this->msg( 'mytalk' )->text(),
616
				'href' => &$usertalkUrlDetails['href'],
617
				'class' => $usertalkUrlDetails['exists'] ? false : 'new',
618
				'active' => ( $usertalkUrlDetails['href'] == $pageurl )
619
			];
620
			$href = self::makeSpecialUrl( 'Preferences' );
621
			$personal_urls['preferences'] = [
622
				'text' => $this->msg( 'mypreferences' )->text(),
623
				'href' => $href,
624
				'active' => ( $href == $pageurl )
625
			];
626
627
			if ( $this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
628
				$href = self::makeSpecialUrl( 'Watchlist' );
629
				$personal_urls['watchlist'] = [
630
					'text' => $this->msg( 'mywatchlist' )->text(),
631
					'href' => $href,
632
					'active' => ( $href == $pageurl )
633
				];
634
			}
635
636
			# We need to do an explicit check for Special:Contributions, as we
637
			# have to match both the title, and the target, which could come
638
			# from request values (Special:Contributions?target=Jimbo_Wales)
639
			# or be specified in "sub page" form
640
			# (Special:Contributions/Jimbo_Wales). The plot
641
			# thickens, because the Title object is altered for special pages,
642
			# so it doesn't contain the original alias-with-subpage.
643
			$origTitle = Title::newFromText( $request->getText( 'title' ) );
644
			if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) {
645
				list( $spName, $spPar ) = SpecialPageFactory::resolveAlias( $origTitle->getText() );
646
				$active = $spName == 'Contributions'
647
					&& ( ( $spPar && $spPar == $this->username )
648
						|| $request->getText( 'target' ) == $this->username );
649
			} else {
650
				$active = false;
651
			}
652
653
			$href = self::makeSpecialUrlSubpage( 'Contributions', $this->username );
654
			$personal_urls['mycontris'] = [
655
				'text' => $this->msg( 'mycontris' )->text(),
656
				'href' => $href,
657
				'active' => $active
658
			];
659
660
			// if we can't set the user, we can't unset it either
661
			if ( $request->getSession()->canSetUser() ) {
662
				$personal_urls['logout'] = [
663
					'text' => $this->msg( 'pt-userlogout' )->text(),
664
					'href' => self::makeSpecialUrl( 'Userlogout',
665
						// userlogout link must always contain an & character, otherwise we might not be able
666
						// to detect a buggy precaching proxy (bug 17790)
667
						$title->isSpecial( 'Preferences' ) ? 'noreturnto' : $returnto ),
668
					'active' => false
669
				];
670
			}
671
		} else {
672
			$useCombinedLoginLink = $this->useCombinedLoginLink();
673
			if ( !$authManager->canCreateAccounts() || !$authManager->canAuthenticateNow() ) {
674
				// don't show combined login/signup link if one of those is actually not available
675
				$useCombinedLoginLink = false;
676
			}
677
678
			$loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink
679
				? 'nav-login-createaccount'
680
				: 'pt-login';
681
682
			$login_url = [
683
				'text' => $this->msg( $loginlink )->text(),
684
				'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
685
				'active' => $title->isSpecial( 'Userlogin' )
686
					|| $title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink,
687
			];
688
			$createaccount_url = [
689
				'text' => $this->msg( 'pt-createaccount' )->text(),
690
				'href' => self::makeSpecialUrl( 'CreateAccount', $returnto ),
691
				'active' => $title->isSpecial( 'CreateAccount' ),
692
			];
693
694
			// No need to show Talk and Contributions to anons if they can't contribute!
695
			if ( User::groupHasPermission( '*', 'edit' ) ) {
696
				// Because of caching, we can't link directly to the IP talk and
697
				// contributions pages. Instead we use the special page shortcuts
698
				// (which work correctly regardless of caching). This means we can't
699
				// determine whether these links are active or not, but since major
700
				// skins (MonoBook, Vector) don't use this information, it's not a
701
				// huge loss.
702
				$personal_urls['anontalk'] = [
703
					'text' => $this->msg( 'anontalk' )->text(),
704
					'href' => self::makeSpecialUrlSubpage( 'Mytalk', false ),
705
					'active' => false
706
				];
707
				$personal_urls['anoncontribs'] = [
708
					'text' => $this->msg( 'anoncontribs' )->text(),
709
					'href' => self::makeSpecialUrlSubpage( 'Mycontributions', false ),
710
					'active' => false
711
				];
712
			}
713
714
			if (
715
				$authManager->canCreateAccounts()
716
				&& $this->getUser()->isAllowed( 'createaccount' )
717
				&& !$useCombinedLoginLink
718
			) {
719
				$personal_urls['createaccount'] = $createaccount_url;
720
			}
721
722
			if ( $authManager->canAuthenticateNow() ) {
723
				$personal_urls['login'] = $login_url;
724
			}
725
		}
726
727
		Hooks::run( 'PersonalUrls', [ &$personal_urls, &$title, $this ] );
728
		return $personal_urls;
729
	}
730
731
	/**
732
	 * Builds an array with tab definition
733
	 *
734
	 * @param Title $title Page Where the tab links to
735
	 * @param string|array $message Message key or an array of message keys (will fall back)
736
	 * @param bool $selected Display the tab as selected
737
	 * @param string $query Query string attached to tab URL
738
	 * @param bool $checkEdit Check if $title exists and mark with .new if one doesn't
739
	 *
740
	 * @return array
741
	 */
742
	function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) {
743
		$classes = [];
744
		if ( $selected ) {
745
			$classes[] = 'selected';
746
		}
747
		if ( $checkEdit && !$title->isKnown() ) {
748
			$classes[] = 'new';
749
			if ( $query !== '' ) {
750
				$query = 'action=edit&redlink=1&' . $query;
751
			} else {
752
				$query = 'action=edit&redlink=1';
753
			}
754
		}
755
756
		$linkClass = MediaWikiServices::getInstance()->getLinkRenderer()->getLinkClasses( $title );
757
758
		// wfMessageFallback will nicely accept $message as an array of fallbacks
759
		// or just a single key
760
		$msg = wfMessageFallback( $message )->setContext( $this->getContext() );
761
		if ( is_array( $message ) ) {
762
			// for hook compatibility just keep the last message name
763
			$message = end( $message );
764
		}
765
		if ( $msg->exists() ) {
766
			$text = $msg->text();
767
		} else {
768
			global $wgContLang;
769
			$text = $wgContLang->getConverter()->convertNamespace(
770
				MWNamespace::getSubject( $title->getNamespace() ) );
771
		}
772
773
		$result = [];
774
		if ( !Hooks::run( 'SkinTemplateTabAction', [ &$this,
775
				$title, $message, $selected, $checkEdit,
776
				&$classes, &$query, &$text, &$result ] ) ) {
777
			return $result;
778
		}
779
780
		$result = [
781
			'class' => implode( ' ', $classes ),
782
			'text' => $text,
783
			'href' => $title->getLocalURL( $query ),
784
			'primary' => true ];
785
		if ( $linkClass !== '' ) {
786
			$result['link-class'] = $linkClass;
787
		}
788
789
		return $result;
790
	}
791
792
	function makeTalkUrlDetails( $name, $urlaction = '' ) {
793
		$title = Title::newFromText( $name );
794
		if ( !is_object( $title ) ) {
795
			throw new MWException( __METHOD__ . " given invalid pagename $name" );
796
		}
797
		$title = $title->getTalkPage();
798
		self::checkTitle( $title, $name );
799
		return [
800
			'href' => $title->getLocalURL( $urlaction ),
801
			'exists' => $title->isKnown(),
802
		];
803
	}
804
805
	/**
806
	 * @todo is this even used?
807
	 */
808 View Code Duplication
	function makeArticleUrlDetails( $name, $urlaction = '' ) {
809
		$title = Title::newFromText( $name );
810
		$title = $title->getSubjectPage();
811
		self::checkTitle( $title, $name );
812
		return [
813
			'href' => $title->getLocalURL( $urlaction ),
814
			'exists' => $title->exists(),
815
		];
816
	}
817
818
	/**
819
	 * a structured array of links usually used for the tabs in a skin
820
	 *
821
	 * There are 4 standard sections
822
	 * namespaces: Used for namespace tabs like special, page, and talk namespaces
823
	 * views: Used for primary page views like read, edit, history
824
	 * actions: Used for most extra page actions like deletion, protection, etc...
825
	 * variants: Used to list the language variants for the page
826
	 *
827
	 * Each section's value is a key/value array of links for that section.
828
	 * The links themselves have these common keys:
829
	 * - class: The css classes to apply to the tab
830
	 * - text: The text to display on the tab
831
	 * - href: The href for the tab to point to
832
	 * - rel: An optional rel= for the tab's link
833
	 * - redundant: If true the tab will be dropped in skins using content_actions
834
	 *   this is useful for tabs like "Read" which only have meaning in skins that
835
	 *   take special meaning from the grouped structure of content_navigation
836
	 *
837
	 * Views also have an extra key which can be used:
838
	 * - primary: If this is not true skins like vector may try to hide the tab
839
	 *            when the user has limited space in their browser window
840
	 *
841
	 * content_navigation using code also expects these ids to be present on the
842
	 * links, however these are usually automatically generated by SkinTemplate
843
	 * itself and are not necessary when using a hook. The only things these may
844
	 * matter to are people modifying content_navigation after it's initial creation:
845
	 * - id: A "preferred" id, most skins are best off outputting this preferred
846
	 *   id for best compatibility.
847
	 * - tooltiponly: This is set to true for some tabs in cases where the system
848
	 *   believes that the accesskey should not be added to the tab.
849
	 *
850
	 * @return array
851
	 */
852
	protected function buildContentNavigationUrls() {
853
		global $wgDisableLangConversion;
854
855
		// Display tabs for the relevant title rather than always the title itself
856
		$title = $this->getRelevantTitle();
857
		$onPage = $title->equals( $this->getTitle() );
858
859
		$out = $this->getOutput();
860
		$request = $this->getRequest();
861
		$user = $this->getUser();
862
863
		$content_navigation = [
864
			'namespaces' => [],
865
			'views' => [],
866
			'actions' => [],
867
			'variants' => []
868
		];
869
870
		// parameters
871
		$action = $request->getVal( 'action', 'view' );
872
873
		$userCanRead = $title->quickUserCan( 'read', $user );
874
875
		$preventActiveTabs = false;
876
		Hooks::run( 'SkinTemplatePreventOtherActiveTabs', [ &$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
			$subjectMsg = [ "nstab-$subjectId" ];
900
			if ( $subjectPage->isMainPage() ) {
901
				array_unshift( $subjectMsg, 'mainpage-nstab' );
902
			}
903
			$content_navigation['namespaces'][$subjectId] = $this->tabAction(
904
				$subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs, '', $userCanRead
905
			);
906
			$content_navigation['namespaces'][$subjectId]['context'] = 'subject';
907
			$content_navigation['namespaces'][$talkId] = $this->tabAction(
908
				$talkPage, [ "nstab-$talkId", 'talk' ], $isTalk && !$preventActiveTabs, '', $userCanRead
909
			);
910
			$content_navigation['namespaces'][$talkId]['context'] = 'talk';
911
912
			if ( $userCanRead ) {
913
				// Adds "view" view link
914
				if ( $title->isKnown() ) {
915
					$content_navigation['views']['view'] = $this->tabAction(
916
						$isTalk ? $talkPage : $subjectPage,
917
						[ "$skname-view-view", 'view' ],
918
						( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true
919
					);
920
					// signal to hide this from simple content_actions
921
					$content_navigation['views']['view']['redundant'] = true;
922
				}
923
924
				$isForeignFile = $title->inNamespace( NS_FILE ) && $this->canUseWikiPage() &&
925
					$this->getWikiPage() instanceof WikiFilePage && !$this->getWikiPage()->isLocal();
926
927
				// If it is a non-local file, show a link to the file in its own repository
928
				// @todo abstract this for remote content that isn't a file
929
				if ( $isForeignFile ) {
930
					$file = $this->getWikiPage()->getFile();
931
					$content_navigation['views']['view-foreign'] = [
932
						'class' => '',
933
						'text' => wfMessageFallback( "$skname-view-foreign", 'view-foreign' )->
934
							setContext( $this->getContext() )->
935
							params( $file->getRepo()->getDisplayName() )->text(),
936
						'href' => $file->getDescriptionUrl(),
937
						'primary' => false,
938
					];
939
				}
940
941
				// Checks if user can edit the current page if it exists or create it otherwise
942
				if ( $title->quickUserCan( 'edit', $user )
943
					&& ( $title->exists() || $title->quickUserCan( 'create', $user ) )
944
				) {
945
					// Builds CSS class for talk page links
946
					$isTalkClass = $isTalk ? ' istalk' : '';
947
					// Whether the user is editing the page
948
					$isEditing = $onPage && ( $action == 'edit' || $action == 'submit' );
949
					// Whether to show the "Add a new section" tab
950
					// Checks if this is a current rev of talk page and is not forced to be hidden
951
					$showNewSection = !$out->forceHideNewSectionLink()
952
						&& ( ( $isTalk && $this->isRevisionCurrent() ) || $out->showNewSectionLink() );
953
					$section = $request->getVal( 'section' );
954
955
					if ( $title->exists()
956
						|| ( $title->getNamespace() == NS_MEDIAWIKI
957
							&& $title->getDefaultMessageText() !== false
958
						)
959
					) {
960
						$msgKey = $isForeignFile ? 'edit-local' : 'edit';
961
					} else {
962
						$msgKey = $isForeignFile ? 'create-local' : 'create';
963
					}
964
					$content_navigation['views']['edit'] = [
965
						'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection )
966
							? 'selected'
967
							: ''
968
						) . $isTalkClass,
969
						'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey )
970
							->setContext( $this->getContext() )->text(),
971
						'href' => $title->getLocalURL( $this->editUrlOptions() ),
972
						'primary' => !$isForeignFile, // don't collapse this in vector
973
					];
974
975
					// section link
976
					if ( $showNewSection ) {
977
						// Adds new section link
978
						// $content_navigation['actions']['addsection']
979
						$content_navigation['views']['addsection'] = [
980
							'class' => ( $isEditing && $section == 'new' ) ? 'selected' : false,
981
							'text' => wfMessageFallback( "$skname-action-addsection", 'addsection' )
982
								->setContext( $this->getContext() )->text(),
983
							'href' => $title->getLocalURL( 'action=edit&section=new' )
984
						];
985
					}
986
				// Checks if the page has some kind of viewable source content
987 View Code Duplication
				} elseif ( $title->hasSourceText() ) {
988
					// Adds view source view link
989
					$content_navigation['views']['viewsource'] = [
990
						'class' => ( $onPage && $action == 'edit' ) ? 'selected' : false,
991
						'text' => wfMessageFallback( "$skname-action-viewsource", 'viewsource' )
992
							->setContext( $this->getContext() )->text(),
993
						'href' => $title->getLocalURL( $this->editUrlOptions() ),
994
						'primary' => true, // don't collapse this in vector
995
					];
996
				}
997
998
				// Checks if the page exists
999
				if ( $title->exists() ) {
1000
					// Adds history view link
1001
					$content_navigation['views']['history'] = [
1002
						'class' => ( $onPage && $action == 'history' ) ? 'selected' : false,
1003
						'text' => wfMessageFallback( "$skname-view-history", 'history_short' )
1004
							->setContext( $this->getContext() )->text(),
1005
						'href' => $title->getLocalURL( 'action=history' ),
1006
					];
1007
1008 View Code Duplication
					if ( $title->quickUserCan( 'delete', $user ) ) {
1009
						$content_navigation['actions']['delete'] = [
1010
							'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false,
1011
							'text' => wfMessageFallback( "$skname-action-delete", 'delete' )
1012
								->setContext( $this->getContext() )->text(),
1013
							'href' => $title->getLocalURL( 'action=delete' )
1014
						];
1015
					}
1016
1017
					if ( $title->quickUserCan( 'move', $user ) ) {
1018
						$moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() );
1019
						$content_navigation['actions']['move'] = [
1020
							'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false,
1021
							'text' => wfMessageFallback( "$skname-action-move", 'move' )
1022
								->setContext( $this->getContext() )->text(),
1023
							'href' => $moveTitle->getLocalURL()
1024
						];
1025
					}
1026
				} else {
1027
					// article doesn't exist or is deleted
1028
					if ( $user->isAllowed( 'deletedhistory' ) ) {
1029
						$n = $title->isDeleted();
1030
						if ( $n ) {
1031
							$undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() );
1032
							// If the user can't undelete but can view deleted
1033
							// history show them a "View .. deleted" tab instead.
1034
							$msgKey = $user->isAllowed( 'undelete' ) ? 'undelete' : 'viewdeleted';
1035
							$content_navigation['actions']['undelete'] = [
1036
								'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false,
1037
								'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" )
1038
									->setContext( $this->getContext() )->numParams( $n )->text(),
1039
								'href' => $undelTitle->getLocalURL()
1040
							];
1041
						}
1042
					}
1043
				}
1044
1045
				if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
1046
					MWNamespace::getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
1047
				) {
1048
					$mode = $title->isProtected() ? 'unprotect' : 'protect';
1049
					$content_navigation['actions'][$mode] = [
1050
						'class' => ( $onPage && $action == $mode ) ? 'selected' : false,
1051
						'text' => wfMessageFallback( "$skname-action-$mode", $mode )
1052
							->setContext( $this->getContext() )->text(),
1053
						'href' => $title->getLocalURL( "action=$mode" )
1054
					];
1055
				}
1056
1057
				// Checks if the user is logged in
1058
				if ( $this->loggedin && $user->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) ) {
1059
					/**
1060
					 * The following actions use messages which, if made particular to
1061
					 * the any specific skins, would break the Ajax code which makes this
1062
					 * action happen entirely inline. OutputPage::getJSVars
1063
					 * defines a set of messages in a javascript object - and these
1064
					 * messages are assumed to be global for all skins. Without making
1065
					 * a change to that procedure these messages will have to remain as
1066
					 * the global versions.
1067
					 */
1068
					$mode = $user->isWatched( $title ) ? 'unwatch' : 'watch';
1069
					$content_navigation['actions'][$mode] = [
1070
						'class' => 'mw-watchlink ' . (
1071
							$onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : ''
1072
						),
1073
						// uses 'watch' or 'unwatch' message
1074
						'text' => $this->msg( $mode )->text(),
1075
						'href' => $title->getLocalURL( [ 'action' => $mode ] )
1076
					];
1077
				}
1078
			}
1079
1080
			Hooks::run( 'SkinTemplateNavigation', [ &$this, &$content_navigation ] );
1081
1082
			if ( $userCanRead && !$wgDisableLangConversion ) {
1083
				$pageLang = $title->getPageLanguage();
1084
				// Gets list of language variants
1085
				$variants = $pageLang->getVariants();
1086
				// Checks that language conversion is enabled and variants exist
1087
				// And if it is not in the special namespace
1088
				if ( count( $variants ) > 1 ) {
1089
					// Gets preferred variant (note that user preference is
1090
					// only possible for wiki content language variant)
1091
					$preferred = $pageLang->getPreferredVariant();
1092
					if ( Action::getActionName( $this ) === 'view' ) {
1093
						$params = $request->getQueryValues();
1094
						unset( $params['title'] );
1095
					} else {
1096
						$params = [];
1097
					}
1098
					// Loops over each variant
1099
					foreach ( $variants as $code ) {
1100
						// Gets variant name from language code
1101
						$varname = $pageLang->getVariantname( $code );
1102
						// Appends variant link
1103
						$content_navigation['variants'][] = [
1104
							'class' => ( $code == $preferred ) ? 'selected' : false,
1105
							'text' => $varname,
1106
							'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ),
1107
							'lang' => wfBCP47( $code ),
1108
							'hreflang' => wfBCP47( $code ),
1109
						];
1110
					}
1111
				}
1112
			}
1113
		} else {
1114
			// If it's not content, it's got to be a special page
1115
			$content_navigation['namespaces']['special'] = [
1116
				'class' => 'selected',
1117
				'text' => $this->msg( 'nstab-special' )->text(),
1118
				'href' => $request->getRequestURL(), // @see: bug 2457, bug 2510
1119
				'context' => 'subject'
1120
			];
1121
1122
			Hooks::run( 'SkinTemplateNavigation::SpecialPage',
1123
				[ &$this, &$content_navigation ] );
1124
		}
1125
1126
		// Equiv to SkinTemplateContentActions
1127
		Hooks::run( 'SkinTemplateNavigation::Universal', [ &$this, &$content_navigation ] );
1128
1129
		// Setup xml ids and tooltip info
1130
		foreach ( $content_navigation as $section => &$links ) {
1131
			foreach ( $links as $key => &$link ) {
1132
				$xmlID = $key;
1133
				if ( isset( $link['context'] ) && $link['context'] == 'subject' ) {
1134
					$xmlID = 'ca-nstab-' . $xmlID;
1135
				} elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) {
1136
					$xmlID = 'ca-talk';
1137
					$link['rel'] = 'discussion';
1138
				} elseif ( $section == 'variants' ) {
1139
					$xmlID = 'ca-varlang-' . $xmlID;
1140
				} else {
1141
					$xmlID = 'ca-' . $xmlID;
1142
				}
1143
				$link['id'] = $xmlID;
1144
			}
1145
		}
1146
1147
		# We don't want to give the watch tab an accesskey if the
1148
		# page is being edited, because that conflicts with the
1149
		# accesskey on the watch checkbox.  We also don't want to
1150
		# give the edit tab an accesskey, because that's fairly
1151
		# superfluous and conflicts with an accesskey (Ctrl-E) often
1152
		# used for editing in Safari.
1153
		if ( in_array( $action, [ 'edit', 'submit' ] ) ) {
1154
			if ( isset( $content_navigation['views']['edit'] ) ) {
1155
				$content_navigation['views']['edit']['tooltiponly'] = true;
1156
			}
1157 View Code Duplication
			if ( isset( $content_navigation['actions']['watch'] ) ) {
1158
				$content_navigation['actions']['watch']['tooltiponly'] = true;
1159
			}
1160 View Code Duplication
			if ( isset( $content_navigation['actions']['unwatch'] ) ) {
1161
				$content_navigation['actions']['unwatch']['tooltiponly'] = true;
1162
			}
1163
		}
1164
1165
		return $content_navigation;
1166
	}
1167
1168
	/**
1169
	 * an array of edit links by default used for the tabs
1170
	 * @param array $content_navigation
1171
	 * @return array
1172
	 */
1173
	private function buildContentActionUrls( $content_navigation ) {
1174
1175
		// content_actions has been replaced with content_navigation for backwards
1176
		// compatibility and also for skins that just want simple tabs content_actions
1177
		// is now built by flattening the content_navigation arrays into one
1178
1179
		$content_actions = [];
1180
1181
		foreach ( $content_navigation as $links ) {
1182
			foreach ( $links as $key => $value ) {
1183
				if ( isset( $value['redundant'] ) && $value['redundant'] ) {
1184
					// Redundant tabs are dropped from content_actions
1185
					continue;
1186
				}
1187
1188
				// content_actions used to have ids built using the "ca-$key" pattern
1189
				// so the xmlID based id is much closer to the actual $key that we want
1190
				// for that reason we'll just strip out the ca- if present and use
1191
				// the latter potion of the "id" as the $key
1192
				if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) {
1193
					$key = substr( $value['id'], 3 );
1194
				}
1195
1196
				if ( isset( $content_actions[$key] ) ) {
1197
					wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " .
1198
						"content_navigation into content_actions.\n" );
1199
					continue;
1200
				}
1201
1202
				$content_actions[$key] = $value;
1203
			}
1204
		}
1205
1206
		return $content_actions;
1207
	}
1208
1209
	/**
1210
	 * build array of common navigation links
1211
	 * @return array
1212
	 */
1213
	protected function buildNavUrls() {
1214
		global $wgUploadNavigationUrl;
1215
1216
		$out = $this->getOutput();
1217
		$request = $this->getRequest();
1218
1219
		$nav_urls = [];
1220
		$nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1221
		if ( $wgUploadNavigationUrl ) {
1222
			$nav_urls['upload'] = [ 'href' => $wgUploadNavigationUrl ];
1223
		} elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
1224
			$nav_urls['upload'] = [ 'href' => self::makeSpecialUrl( 'Upload' ) ];
1225
		} else {
1226
			$nav_urls['upload'] = false;
1227
		}
1228
		$nav_urls['specialpages'] = [ 'href' => self::makeSpecialUrl( 'Specialpages' ) ];
1229
1230
		$nav_urls['print'] = false;
1231
		$nav_urls['permalink'] = false;
1232
		$nav_urls['info'] = false;
1233
		$nav_urls['whatlinkshere'] = false;
1234
		$nav_urls['recentchangeslinked'] = false;
1235
		$nav_urls['contributions'] = false;
1236
		$nav_urls['log'] = false;
1237
		$nav_urls['blockip'] = false;
1238
		$nav_urls['emailuser'] = false;
1239
		$nav_urls['userrights'] = false;
1240
1241
		// A print stylesheet is attached to all pages, but nobody ever
1242
		// figures that out. :)  Add a link...
1243
		if ( !$out->isPrintable() && ( $out->isArticle() || $this->getTitle()->isSpecialPage() ) ) {
1244
			$nav_urls['print'] = [
1245
				'text' => $this->msg( 'printableversion' )->text(),
1246
				'href' => $this->getTitle()->getLocalURL(
1247
					$request->appendQueryValue( 'printable', 'yes' ) )
1248
			];
1249
		}
1250
1251
		if ( $out->isArticle() ) {
1252
			// Also add a "permalink" while we're at it
1253
			$revid = $this->getRevisionId();
1254
			if ( $revid ) {
1255
				$nav_urls['permalink'] = [
1256
					'text' => $this->msg( 'permalink' )->text(),
1257
					'href' => $this->getTitle()->getLocalURL( "oldid=$revid" )
1258
				];
1259
			}
1260
1261
			// Use the copy of revision ID in case this undocumented, shady hook tries to mess with internals
1262
			Hooks::run( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink',
1263
				[ &$this, &$nav_urls, &$revid, &$revid ] );
1264
		}
1265
1266
		if ( $out->isArticleRelated() ) {
1267
			$nav_urls['whatlinkshere'] = [
1268
				'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalURL()
1269
			];
1270
1271
			$nav_urls['info'] = [
1272
				'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1273
				'href' => $this->getTitle()->getLocalURL( "action=info" )
1274
			];
1275
1276
			if ( $this->getTitle()->exists() ) {
1277
				$nav_urls['recentchangeslinked'] = [
1278
					'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalURL()
1279
				];
1280
			}
1281
		}
1282
1283
		$user = $this->getRelevantUser();
1284
		if ( $user ) {
1285
			$rootUser = $user->getName();
1286
1287
			$nav_urls['contributions'] = [
1288
				'text' => $this->msg( 'contributions', $rootUser )->text(),
1289
				'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1290
				'tooltip-params' => [ $rootUser ],
1291
			];
1292
1293
			$nav_urls['log'] = [
1294
				'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
1295
			];
1296
1297
			if ( $this->getUser()->isAllowed( 'block' ) ) {
1298
				$nav_urls['blockip'] = [
1299
					'text' => $this->msg( 'blockip', $rootUser )->text(),
1300
					'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser )
1301
				];
1302
			}
1303
1304
			if ( $this->showEmailUser( $user ) ) {
1305
				$nav_urls['emailuser'] = [
1306
					'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1307
					'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1308
					'tooltip-params' => [ $rootUser ],
1309
				];
1310
			}
1311
1312
			if ( !$user->isAnon() ) {
1313
				$sur = new UserrightsPage;
1314
				$sur->setContext( $this->getContext() );
1315
				if ( $sur->userCanExecute( $this->getUser() ) ) {
1316
					$nav_urls['userrights'] = [
1317
						'text' => $this->msg( 'tool-link-userrights', $this->getUser()->getName() )->text(),
1318
						'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
1319
					];
1320
				}
1321
			}
1322
		}
1323
1324
		return $nav_urls;
1325
	}
1326
1327
	/**
1328
	 * Generate strings used for xml 'id' names
1329
	 * @return string
1330
	 */
1331
	protected function getNameSpaceKey() {
1332
		return $this->getTitle()->getNamespaceKey();
1333
	}
1334
}
1335