Completed
Branch master (5cbada)
by
unknown
28:59
created

SkinTemplate   F

Complexity

Total Complexity 190

Size/Duplication

Total Lines 1286
Duplicated Lines 3.03 %

Coupling/Cohesion

Components 1
Dependencies 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 39
loc 1286
rs 0.6314
c 2
b 0
f 0
wmc 190
lcom 1
cbo 20

19 Methods

Rating   Name   Duplication   Size   Complexity  
F buildPersonalUrls() 0 156 25
A setupSkinUserCss() 0 18 3
A setupTemplate() 0 3 1
C getLanguages() 0 93 10
B setupTemplateForOutput() 6 32 3
B outputPage() 0 26 3
A wrapHTML() 0 17 3
F prepareQuickTemplate() 0 223 21
A getPersonalToolsList() 0 9 2
A formatLanguageName() 0 3 1
A printOrError() 0 3 1
A useCombinedLoginLink() 0 4 1
C tabAction() 0 42 8
A makeTalkUrlDetails() 0 12 2
A makeArticleUrlDetails() 9 9 1
F buildContentNavigationUrls() 24 314 80
C buildContentActionUrls() 0 35 8
F buildNavUrls() 0 111 16
A getNameSpaceKey() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SkinTemplate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SkinTemplate, and based on these observations, apply Extract Interface, too.

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

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
408
						$this->getContext() )->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) );
409
				} else {
410
					$tpl->set( 'lastmod', $this->lastModified() );
411
				}
412
			}
413
			$tpl->set( 'copyright', $this->getCopyright() );
414
		}
415
416
		$tpl->set( 'copyrightico', $this->getCopyrightIcon() );
417
		$tpl->set( 'poweredbyico', $this->getPoweredBy() );
418
		$tpl->set( 'disclaimer', $this->disclaimerLink() );
419
		$tpl->set( 'privacy', $this->privacyLink() );
420
		$tpl->set( 'about', $this->aboutLink() );
421
422
		$tpl->set( 'footerlinks', [
423
			'info' => [
424
				'lastmod',
425
				'numberofwatchingusers',
426
				'credits',
427
				'copyright',
428
			],
429
			'places' => [
430
				'privacy',
431
				'about',
432
				'disclaimer',
433
			],
434
		] );
435
436
		global $wgFooterIcons;
437
		$tpl->set( 'footericons', $wgFooterIcons );
438
		foreach ( $tpl->data['footericons'] as $footerIconsKey => &$footerIconsBlock ) {
439
			if ( count( $footerIconsBlock ) > 0 ) {
440
				foreach ( $footerIconsBlock as &$footerIcon ) {
441
					if ( isset( $footerIcon['src'] ) ) {
442
						if ( !isset( $footerIcon['width'] ) ) {
443
							$footerIcon['width'] = 88;
444
						}
445
						if ( !isset( $footerIcon['height'] ) ) {
446
							$footerIcon['height'] = 31;
447
						}
448
					}
449
				}
450
			} else {
451
				unset( $tpl->data['footericons'][$footerIconsKey] );
452
			}
453
		}
454
455
		$tpl->set( 'indicators', $out->getIndicators() );
456
457
		$tpl->set( 'sitenotice', $this->getSiteNotice() );
458
		$tpl->set( 'bottomscripts', $this->bottomScripts() );
459
		$tpl->set( 'printfooter', $this->printSource() );
460
		// Wrap the bodyText with #mw-content-text element
461
		$out->mBodytext = $this->wrapHTML( $title, $out->mBodytext );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 296 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...
462
		$tpl->setRef( 'bodytext', $out->mBodytext );
463
464
		$language_urls = $this->getLanguages();
465
		if ( count( $language_urls ) ) {
466
			$tpl->setRef( 'language_urls', $language_urls );
467
		} else {
468
			$tpl->set( 'language_urls', false );
469
		}
470
471
		# Personal toolbar
472
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
473
		$content_navigation = $this->buildContentNavigationUrls();
474
		$content_actions = $this->buildContentActionUrls( $content_navigation );
475
		$tpl->setRef( 'content_navigation', $content_navigation );
476
		$tpl->setRef( 'content_actions', $content_actions );
477
478
		$tpl->set( 'sidebar', $this->buildSidebar() );
479
		$tpl->set( 'nav_urls', $this->buildNavUrls() );
480
481
		// Set the head scripts near the end, in case the above actions resulted in added scripts
482
		$tpl->set( 'headelement', $out->headElement( $this ) );
483
484
		$tpl->set( 'debug', '' );
485
		$tpl->set( 'debughtml', $this->generateDebugHTML() );
486
		$tpl->set( 'reporttime', wfReportTime() );
487
488
		// original version by hansm
489
		if ( !Hooks::run( 'SkinTemplateOutputPageBeforeExec', [ &$this, &$tpl ] ) ) {
490
			wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" );
491
		}
492
493
		// Set the bodytext to another key so that skins can just output it on its own
494
		// and output printfooter and debughtml separately
495
		$tpl->set( 'bodycontent', $tpl->data['bodytext'] );
496
497
		// Append printfooter and debughtml onto bodytext so that skins that
498
		// were already using bodytext before they were split out don't suddenly
499
		// start not outputting information.
500
		$tpl->data['bodytext'] .= Html::rawElement(
501
			'div',
502
			[ 'class' => 'printfooter' ],
503
			"\n{$tpl->data['printfooter']}"
504
		) . "\n";
505
		$tpl->data['bodytext'] .= $tpl->data['debughtml'];
506
507
		// allow extensions adding stuff after the page content.
508
		// See Skin::afterContentHook() for further documentation.
509
		$tpl->set( 'dataAfterContent', $this->afterContentHook() );
510
511
		return $tpl;
512
	}
513
514
	/**
515
	 * Get the HTML for the p-personal list
516
	 * @return string
517
	 */
518
	public function getPersonalToolsList() {
519
		$tpl = $this->setupTemplateForOutput();
520
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
521
		$html = '';
522
		foreach ( $tpl->getPersonalTools() as $key => $item ) {
523
			$html .= $tpl->makeListItem( $key, $item );
524
		}
525
		return $html;
526
	}
527
528
	/**
529
	 * Format language name for use in sidebar interlanguage links list.
530
	 * By default it is capitalized.
531
	 *
532
	 * @param string $name Language name, e.g. "English" or "español"
533
	 * @return string
534
	 * @private
535
	 */
536
	function formatLanguageName( $name ) {
537
		return $this->getLanguage()->ucfirst( $name );
538
	}
539
540
	/**
541
	 * Output the string, or print error message if it's
542
	 * an error object of the appropriate type.
543
	 * For the base class, assume strings all around.
544
	 *
545
	 * @param string $str
546
	 * @private
547
	 */
548
	function printOrError( $str ) {
549
		echo $str;
550
	}
551
552
	/**
553
	 * Output a boolean indicating if buildPersonalUrls should output separate
554
	 * login and create account links or output a combined link
555
	 * By default we simply return a global config setting that affects most skins
556
	 * This is setup as a method so that like with $wgLogo and getLogo() a skin
557
	 * can override this setting and always output one or the other if it has
558
	 * a reason it can't output one of the two modes.
559
	 * @return bool
560
	 */
561
	function useCombinedLoginLink() {
562
		global $wgUseCombinedLoginLink;
563
		return $wgUseCombinedLoginLink;
564
	}
565
566
	/**
567
	 * build array of urls for personal toolbar
568
	 * @return array
569
	 */
570
	protected function buildPersonalUrls() {
571
		$title = $this->getTitle();
572
		$request = $this->getRequest();
573
		$pageurl = $title->getLocalURL();
574
575
		/* set up the default links for the personal toolbar */
576
		$personal_urls = [];
577
578
		# Due to bug 32276, if a user does not have read permissions,
579
		# $this->getTitle() will just give Special:Badtitle, which is
580
		# not especially useful as a returnto parameter. Use the title
581
		# from the request instead, if there was one.
582
		if ( $this->getUser()->isAllowed( 'read' ) ) {
583
			$page = $this->getTitle();
584
		} else {
585
			$page = Title::newFromText( $request->getVal( 'title', '' ) );
586
		}
587
		$page = $request->getVal( 'returnto', $page );
0 ignored issues
show
Bug introduced by
It seems like $page can also be of type object<Title>; however, WebRequest::getVal() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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