Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/skins/SkinTemplate.php (5 issues)

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
		$request = $this->getRequest();
200
		$user = $this->getUser();
201
		$title = $this->getTitle();
202
203
		$tpl = $this->setupTemplate( $this->template, 'skins' );
204
205
		$this->thispage = $title->getPrefixedDBkey();
206
		$this->titletxt = $title->getPrefixedText();
207
		$this->userpage = $user->getUserPage()->getPrefixedText();
208
		$query = [];
209 View Code Duplication
		if ( !$request->wasPosted() ) {
210
			$query = $request->getValues();
211
			unset( $query['title'] );
212
			unset( $query['returnto'] );
213
			unset( $query['returntoquery'] );
214
		}
215
		$this->thisquery = wfArrayToCgi( $query );
216
		$this->loggedin = $user->isLoggedIn();
217
		$this->username = $user->getName();
218
219
		if ( $this->loggedin ) {
220
			$this->userpageUrlDetails = self::makeUrlDetails( $this->userpage );
221
		} else {
222
			# This won't be used in the standard skins, but we define it to preserve the interface
223
			# To save time, we check for existence
224
			$this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage );
225
		}
226
227
		return $tpl;
228
	}
229
230
	/**
231
	 * initialize various variables and generate the template
232
	 *
233
	 * @param OutputPage $out
234
	 */
235
	function outputPage( OutputPage $out = null ) {
236
		Profiler::instance()->setTemplated( true );
237
238
		$oldContext = null;
239
		if ( $out !== null ) {
240
			// Deprecated since 1.20, note added in 1.25
241
			wfDeprecated( __METHOD__, '1.25' );
242
			$oldContext = $this->getContext();
243
			$this->setContext( $out->getContext() );
244
		}
245
246
		$out = $this->getOutput();
247
248
		$this->initPage( $out );
249
		$tpl = $this->prepareQuickTemplate();
250
		// execute template
251
		$res = $tpl->execute();
252
253
		// result may be an error
254
		$this->printOrError( $res );
255
256
		if ( $oldContext ) {
257
			$this->setContext( $oldContext );
258
		}
259
	}
260
261
	/**
262
	 * Wrap the body text with language information and identifiable element
263
	 *
264
	 * @param Title $title
265
	 * @param string $html body text
266
	 * @return string html
267
	 */
268
	protected function wrapHTML( $title, $html ) {
269
		# An ID that includes the actual body text; without categories, contentSub, ...
270
		$realBodyAttribs = [ 'id' => 'mw-content-text' ];
271
272
		# Add a mw-content-ltr/rtl class to be able to style based on text direction
273
		# when the content is different from the UI language, i.e.:
274
		# not for special pages or file pages AND only when viewing
275
		if ( !in_array( $title->getNamespace(), [ NS_SPECIAL, NS_FILE ] ) &&
276
			Action::getActionName( $this ) === 'view' ) {
277
			$pageLang = $title->getPageViewLanguage();
278
			$realBodyAttribs['lang'] = $pageLang->getHtmlCode();
279
			$realBodyAttribs['dir'] = $pageLang->getDir();
280
			$realBodyAttribs['class'] = 'mw-content-' . $pageLang->getDir();
281
		}
282
283
		return Html::rawElement( 'div', $realBodyAttribs, $html );
284
	}
285
286
	/**
287
	 * initialize various variables and generate the template
288
	 *
289
	 * @since 1.23
290
	 * @return QuickTemplate The template to be executed by outputPage
291
	 */
292
	protected function prepareQuickTemplate() {
293
		global $wgContLang, $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType,
294
			$wgSitename, $wgLogo, $wgMaxCredits,
295
			$wgShowCreditsIfMax, $wgArticlePath,
296
			$wgScriptPath, $wgServer;
297
298
		$title = $this->getTitle();
299
		$request = $this->getRequest();
300
		$out = $this->getOutput();
301
		$tpl = $this->setupTemplateForOutput();
302
303
		$tpl->set( 'title', $out->getPageTitle() );
304
		$tpl->set( 'pagetitle', $out->getHTMLTitle() );
305
		$tpl->set( 'displaytitle', $out->mPageLinkTitle );
306
307
		$tpl->setRef( 'thispage', $this->thispage );
308
		$tpl->setRef( 'titleprefixeddbkey', $this->thispage );
309
		$tpl->set( 'titletext', $title->getText() );
310
		$tpl->set( 'articleid', $title->getArticleID() );
311
312
		$tpl->set( 'isarticle', $out->isArticle() );
313
314
		$subpagestr = $this->subPageSubtitle();
315
		if ( $subpagestr !== '' ) {
316
			$subpagestr = '<span class="subpages">' . $subpagestr . '</span>';
317
		}
318
		$tpl->set( 'subtitle', $subpagestr . $out->getSubtitle() );
319
320
		$undelete = $this->getUndeleteLink();
321
		if ( $undelete === '' ) {
322
			$tpl->set( 'undelete', '' );
323
		} else {
324
			$tpl->set( 'undelete', '<span class="subpages">' . $undelete . '</span>' );
325
		}
326
327
		$tpl->set( 'catlinks', $this->getCategories() );
328
		if ( $out->isSyndicated() ) {
329
			$feeds = [];
330
			foreach ( $out->getSyndicationLinks() as $format => $link ) {
331
				$feeds[$format] = [
332
					// Messages: feed-atom, feed-rss
333
					'text' => $this->msg( "feed-$format" )->text(),
334
					'href' => $link
335
				];
336
			}
337
			$tpl->setRef( 'feeds', $feeds );
338
		} else {
339
			$tpl->set( 'feeds', false );
340
		}
341
342
		$tpl->setRef( 'mimetype', $wgMimeType );
343
		$tpl->setRef( 'jsmimetype', $wgJsMimeType );
344
		$tpl->set( 'charset', 'UTF-8' );
345
		$tpl->setRef( 'wgScript', $wgScript );
346
		$tpl->setRef( 'skinname', $this->skinname );
347
		$tpl->set( 'skinclass', get_class( $this ) );
348
		$tpl->setRef( 'skin', $this );
349
		$tpl->setRef( 'stylename', $this->stylename );
350
		$tpl->set( 'printable', $out->isPrintable() );
351
		$tpl->set( 'handheld', $request->getBool( 'handheld' ) );
352
		$tpl->setRef( 'loggedin', $this->loggedin );
353
		$tpl->set( 'notspecialpage', !$title->isSpecialPage() );
354
		$tpl->set( 'searchaction', $this->escapeSearchLink() );
355
		$tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey() );
356
		$tpl->set( 'search', trim( $request->getVal( 'search' ) ) );
357
		$tpl->setRef( 'stylepath', $wgStylePath );
358
		$tpl->setRef( 'articlepath', $wgArticlePath );
359
		$tpl->setRef( 'scriptpath', $wgScriptPath );
360
		$tpl->setRef( 'serverurl', $wgServer );
361
		$tpl->setRef( 'logopath', $wgLogo );
362
		$tpl->setRef( 'sitename', $wgSitename );
363
364
		$userLang = $this->getLanguage();
365
		$userLangCode = $userLang->getHtmlCode();
366
		$userLangDir = $userLang->getDir();
367
368
		$tpl->set( 'lang', $userLangCode );
369
		$tpl->set( 'dir', $userLangDir );
370
		$tpl->set( 'rtl', $userLang->isRTL() );
371
372
		$tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
373
		$tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed
374
		$tpl->set( 'username', $this->loggedin ? $this->username : null );
375
		$tpl->setRef( 'userpage', $this->userpage );
376
		$tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] );
377
		$tpl->set( 'userlang', $userLangCode );
378
379
		// Users can have their language set differently than the
380
		// content of the wiki. For these users, tell the web browser
381
		// that interface elements are in a different language.
382
		$tpl->set( 'userlangattributes', '' );
383
		$tpl->set( 'specialpageattributes', '' ); # obsolete
384
		// Used by VectorBeta to insert HTML before content but after the
385
		// heading for the page title. Defaults to empty string.
386
		$tpl->set( 'prebodyhtml', '' );
387
388
		if ( $userLangCode !== $wgContLang->getHtmlCode() || $userLangDir !== $wgContLang->getDir() ) {
389
			$escUserlang = htmlspecialchars( $userLangCode );
390
			$escUserdir = htmlspecialchars( $userLangDir );
391
			// Attributes must be in double quotes because htmlspecialchars() doesn't
392
			// escape single quotes
393
			$attrs = " lang=\"$escUserlang\" dir=\"$escUserdir\"";
394
			$tpl->set( 'userlangattributes', $attrs );
395
		}
396
397
		$tpl->set( 'newtalk', $this->getNewtalks() );
398
		$tpl->set( 'logo', $this->logoText() );
399
400
		$tpl->set( 'copyright', false );
401
		// No longer used
402
		$tpl->set( 'viewcount', false );
403
		$tpl->set( 'lastmod', false );
404
		$tpl->set( 'credits', false );
405
		$tpl->set( 'numberofwatchingusers', false );
406
		if ( $out->isArticle() && $title->exists() ) {
407
			if ( $this->isRevisionCurrent() ) {
408
				if ( $wgMaxCredits != 0 ) {
409
					$tpl->set( 'credits', Action::factory( 'credits', $this->getWikiPage(),
0 ignored issues
show
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...
410
						$this->getContext() )->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) );
411
				} else {
412
					$tpl->set( 'lastmod', $this->lastModified() );
413
				}
414
			}
415
			$tpl->set( 'copyright', $this->getCopyright() );
416
		}
417
418
		$tpl->set( 'copyrightico', $this->getCopyrightIcon() );
419
		$tpl->set( 'poweredbyico', $this->getPoweredBy() );
420
		$tpl->set( 'disclaimer', $this->disclaimerLink() );
421
		$tpl->set( 'privacy', $this->privacyLink() );
422
		$tpl->set( 'about', $this->aboutLink() );
423
424
		$tpl->set( 'footerlinks', [
425
			'info' => [
426
				'lastmod',
427
				'numberofwatchingusers',
428
				'credits',
429
				'copyright',
430
			],
431
			'places' => [
432
				'privacy',
433
				'about',
434
				'disclaimer',
435
			],
436
		] );
437
438
		global $wgFooterIcons;
439
		$tpl->set( 'footericons', $wgFooterIcons );
440
		foreach ( $tpl->data['footericons'] as $footerIconsKey => &$footerIconsBlock ) {
441
			if ( count( $footerIconsBlock ) > 0 ) {
442
				foreach ( $footerIconsBlock as &$footerIcon ) {
443
					if ( isset( $footerIcon['src'] ) ) {
444
						if ( !isset( $footerIcon['width'] ) ) {
445
							$footerIcon['width'] = 88;
446
						}
447
						if ( !isset( $footerIcon['height'] ) ) {
448
							$footerIcon['height'] = 31;
449
						}
450
					}
451
				}
452
			} else {
453
				unset( $tpl->data['footericons'][$footerIconsKey] );
454
			}
455
		}
456
457
		$tpl->set( 'indicators', $out->getIndicators() );
458
459
		$tpl->set( 'sitenotice', $this->getSiteNotice() );
460
		$tpl->set( 'printfooter', $this->printSource() );
461
		// Wrap the bodyText with #mw-content-text element
462
		$out->mBodytext = $this->wrapHTML( $title, $out->mBodytext );
0 ignored issues
show
It seems like $title defined by $this->getTitle() on line 298 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...
463
		$tpl->setRef( 'bodytext', $out->mBodytext );
464
465
		$language_urls = $this->getLanguages();
466
		if ( count( $language_urls ) ) {
467
			$tpl->setRef( 'language_urls', $language_urls );
468
		} else {
469
			$tpl->set( 'language_urls', false );
470
		}
471
472
		# Personal toolbar
473
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
474
		$content_navigation = $this->buildContentNavigationUrls();
475
		$content_actions = $this->buildContentActionUrls( $content_navigation );
476
		$tpl->setRef( 'content_navigation', $content_navigation );
477
		$tpl->setRef( 'content_actions', $content_actions );
478
479
		$tpl->set( 'sidebar', $this->buildSidebar() );
480
		$tpl->set( 'nav_urls', $this->buildNavUrls() );
481
482
		// Do this last in case hooks above add bottom scripts
483
		$tpl->set( 'bottomscripts', $this->bottomScripts() );
484
485
		// Set the head scripts near the end, in case the above actions resulted in added scripts
486
		$tpl->set( 'headelement', $out->headElement( $this ) );
487
488
		$tpl->set( 'debug', '' );
489
		$tpl->set( 'debughtml', $this->generateDebugHTML() );
490
		$tpl->set( 'reporttime', wfReportTime() );
491
492
		// original version by hansm
493
		if ( !Hooks::run( 'SkinTemplateOutputPageBeforeExec', [ &$this, &$tpl ] ) ) {
494
			wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" );
495
		}
496
497
		// Set the bodytext to another key so that skins can just output it on its own
498
		// and output printfooter and debughtml separately
499
		$tpl->set( 'bodycontent', $tpl->data['bodytext'] );
500
501
		// Append printfooter and debughtml onto bodytext so that skins that
502
		// were already using bodytext before they were split out don't suddenly
503
		// start not outputting information.
504
		$tpl->data['bodytext'] .= Html::rawElement(
505
			'div',
506
			[ 'class' => 'printfooter' ],
507
			"\n{$tpl->data['printfooter']}"
508
		) . "\n";
509
		$tpl->data['bodytext'] .= $tpl->data['debughtml'];
510
511
		// allow extensions adding stuff after the page content.
512
		// See Skin::afterContentHook() for further documentation.
513
		$tpl->set( 'dataAfterContent', $this->afterContentHook() );
514
515
		return $tpl;
516
	}
517
518
	/**
519
	 * Get the HTML for the p-personal list
520
	 * @return string
521
	 */
522
	public function getPersonalToolsList() {
523
		$tpl = $this->setupTemplateForOutput();
524
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
525
		$html = '';
526
		foreach ( $tpl->getPersonalTools() as $key => $item ) {
527
			$html .= $tpl->makeListItem( $key, $item );
528
		}
529
		return $html;
530
	}
531
532
	/**
533
	 * Format language name for use in sidebar interlanguage links list.
534
	 * By default it is capitalized.
535
	 *
536
	 * @param string $name Language name, e.g. "English" or "español"
537
	 * @return string
538
	 * @private
539
	 */
540
	function formatLanguageName( $name ) {
541
		return $this->getLanguage()->ucfirst( $name );
542
	}
543
544
	/**
545
	 * Output the string, or print error message if it's
546
	 * an error object of the appropriate type.
547
	 * For the base class, assume strings all around.
548
	 *
549
	 * @param string $str
550
	 * @private
551
	 */
552
	function printOrError( $str ) {
553
		echo $str;
554
	}
555
556
	/**
557
	 * Output a boolean indicating if buildPersonalUrls should output separate
558
	 * login and create account links or output a combined link
559
	 * By default we simply return a global config setting that affects most skins
560
	 * This is setup as a method so that like with $wgLogo and getLogo() a skin
561
	 * can override this setting and always output one or the other if it has
562
	 * a reason it can't output one of the two modes.
563
	 * @return bool
564
	 */
565
	function useCombinedLoginLink() {
566
		global $wgUseCombinedLoginLink;
567
		return $wgUseCombinedLoginLink;
568
	}
569
570
	/**
571
	 * build array of urls for personal toolbar
572
	 * @return array
573
	 */
574
	protected function buildPersonalUrls() {
575
		$title = $this->getTitle();
576
		$request = $this->getRequest();
577
		$pageurl = $title->getLocalURL();
578
		$authManager = AuthManager::singleton();
579
580
		/* set up the default links for the personal toolbar */
581
		$personal_urls = [];
582
583
		# Due to bug 32276, if a user does not have read permissions,
584
		# $this->getTitle() will just give Special:Badtitle, which is
585
		# not especially useful as a returnto parameter. Use the title
586
		# from the request instead, if there was one.
587
		if ( $this->getUser()->isAllowed( 'read' ) ) {
588
			$page = $this->getTitle();
589
		} else {
590
			$page = Title::newFromText( $request->getVal( 'title', '' ) );
591
		}
592
		$page = $request->getVal( 'returnto', $page );
0 ignored issues
show
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...
593
		$a = [];
594
		if ( strval( $page ) !== '' ) {
595
			$a['returnto'] = $page;
596
			$query = $request->getVal( 'returntoquery', $this->thisquery );
597
			if ( $query != '' ) {
598
				$a['returntoquery'] = $query;
599
			}
600
		}
601
602
		$returnto = wfArrayToCgi( $a );
603
		if ( $this->loggedin ) {
604
			$personal_urls['userpage'] = [
605
				'text' => $this->username,
606
				'href' => &$this->userpageUrlDetails['href'],
607
				'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
608
				'active' => ( $this->userpageUrlDetails['href'] == $pageurl ),
609
				'dir' => 'auto'
610
			];
611
			$usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage );
612
			$personal_urls['mytalk'] = [
613
				'text' => $this->msg( 'mytalk' )->text(),
614
				'href' => &$usertalkUrlDetails['href'],
615
				'class' => $usertalkUrlDetails['exists'] ? false : 'new',
616
				'active' => ( $usertalkUrlDetails['href'] == $pageurl )
617
			];
618
			$href = self::makeSpecialUrl( 'Preferences' );
619
			$personal_urls['preferences'] = [
620
				'text' => $this->msg( 'mypreferences' )->text(),
621
				'href' => $href,
622
				'active' => ( $href == $pageurl )
623
			];
624
625
			if ( $this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
626
				$href = self::makeSpecialUrl( 'Watchlist' );
627
				$personal_urls['watchlist'] = [
628
					'text' => $this->msg( 'mywatchlist' )->text(),
629
					'href' => $href,
630
					'active' => ( $href == $pageurl )
631
				];
632
			}
633
634
			# We need to do an explicit check for Special:Contributions, as we
635
			# have to match both the title, and the target, which could come
636
			# from request values (Special:Contributions?target=Jimbo_Wales)
637
			# or be specified in "sub page" form
638
			# (Special:Contributions/Jimbo_Wales). The plot
639
			# thickens, because the Title object is altered for special pages,
640
			# so it doesn't contain the original alias-with-subpage.
641
			$origTitle = Title::newFromText( $request->getText( 'title' ) );
642
			if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) {
643
				list( $spName, $spPar ) = SpecialPageFactory::resolveAlias( $origTitle->getText() );
644
				$active = $spName == 'Contributions'
645
					&& ( ( $spPar && $spPar == $this->username )
646
						|| $request->getText( 'target' ) == $this->username );
647
			} else {
648
				$active = false;
649
			}
650
651
			$href = self::makeSpecialUrlSubpage( 'Contributions', $this->username );
652
			$personal_urls['mycontris'] = [
653
				'text' => $this->msg( 'mycontris' )->text(),
654
				'href' => $href,
655
				'active' => $active
656
			];
657
658
			// if we can't set the user, we can't unset it either
659
			if ( $request->getSession()->canSetUser() ) {
660
				$personal_urls['logout'] = [
661
					'text' => $this->msg( 'pt-userlogout' )->text(),
662
					'href' => self::makeSpecialUrl( 'Userlogout',
663
						// userlogout link must always contain an & character, otherwise we might not be able
664
						// to detect a buggy precaching proxy (bug 17790)
665
						$title->isSpecial( 'Preferences' ) ? 'noreturnto' : $returnto ),
666
					'active' => false
667
				];
668
			}
669
		} else {
670
			$useCombinedLoginLink = $this->useCombinedLoginLink();
671
			if ( !$authManager->canCreateAccounts() || !$authManager->canAuthenticateNow() ) {
672
				// don't show combined login/signup link if one of those is actually not available
673
				$useCombinedLoginLink = false;
674
			}
675
676
			$loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink
677
				? 'nav-login-createaccount'
678
				: 'pt-login';
679
680
			$login_url = [
681
				'text' => $this->msg( $loginlink )->text(),
682
				'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
683
				'active' => $title->isSpecial( 'Userlogin' )
684
					|| $title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink,
685
			];
686
			$createaccount_url = [
687
				'text' => $this->msg( 'pt-createaccount' )->text(),
688
				'href' => self::makeSpecialUrl( 'CreateAccount', $returnto ),
689
				'active' => $title->isSpecial( 'CreateAccount' ),
690
			];
691
692
			// No need to show Talk and Contributions to anons if they can't contribute!
693
			if ( User::groupHasPermission( '*', 'edit' ) ) {
694
				// Because of caching, we can't link directly to the IP talk and
695
				// contributions pages. Instead we use the special page shortcuts
696
				// (which work correctly regardless of caching). This means we can't
697
				// determine whether these links are active or not, but since major
698
				// skins (MonoBook, Vector) don't use this information, it's not a
699
				// huge loss.
700
				$personal_urls['anontalk'] = [
701
					'text' => $this->msg( 'anontalk' )->text(),
702
					'href' => self::makeSpecialUrlSubpage( 'Mytalk', false ),
703
					'active' => false
704
				];
705
				$personal_urls['anoncontribs'] = [
706
					'text' => $this->msg( 'anoncontribs' )->text(),
707
					'href' => self::makeSpecialUrlSubpage( 'Mycontributions', false ),
708
					'active' => false
709
				];
710
			}
711
712
			if (
713
				$authManager->canCreateAccounts()
714
				&& $this->getUser()->isAllowed( 'createaccount' )
715
				&& !$useCombinedLoginLink
716
			) {
717
				$personal_urls['createaccount'] = $createaccount_url;
718
			}
719
720
			if ( $authManager->canAuthenticateNow() ) {
721
				$personal_urls['login'] = $login_url;
722
			}
723
		}
724
725
		Hooks::run( 'PersonalUrls', [ &$personal_urls, &$title, $this ] );
726
		return $personal_urls;
727
	}
728
729
	/**
730
	 * Builds an array with tab definition
731
	 *
732
	 * @param Title $title Page Where the tab links to
733
	 * @param string|array $message Message key or an array of message keys (will fall back)
734
	 * @param bool $selected Display the tab as selected
735
	 * @param string $query Query string attached to tab URL
736
	 * @param bool $checkEdit Check if $title exists and mark with .new if one doesn't
737
	 *
738
	 * @return array
739
	 */
740
	function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) {
741
		$classes = [];
742
		if ( $selected ) {
743
			$classes[] = 'selected';
744
		}
745
		if ( $checkEdit && !$title->isKnown() ) {
746
			$classes[] = 'new';
747
			if ( $query !== '' ) {
748
				$query = 'action=edit&redlink=1&' . $query;
749
			} else {
750
				$query = 'action=edit&redlink=1';
751
			}
752
		}
753
754
		$linkClass = MediaWikiServices::getInstance()->getLinkRenderer()->getLinkClasses( $title );
755
756
		// wfMessageFallback will nicely accept $message as an array of fallbacks
757
		// or just a single key
758
		$msg = wfMessageFallback( $message )->setContext( $this->getContext() );
759
		if ( is_array( $message ) ) {
760
			// for hook compatibility just keep the last message name
761
			$message = end( $message );
762
		}
763
		if ( $msg->exists() ) {
764
			$text = $msg->text();
765
		} else {
766
			global $wgContLang;
767
			$text = $wgContLang->getConverter()->convertNamespace(
768
				MWNamespace::getSubject( $title->getNamespace() ) );
769
		}
770
771
		$result = [];
772
		if ( !Hooks::run( 'SkinTemplateTabAction', [ &$this,
773
				$title, $message, $selected, $checkEdit,
774
				&$classes, &$query, &$text, &$result ] ) ) {
775
			return $result;
776
		}
777
778
		$result = [
779
			'class' => implode( ' ', $classes ),
780
			'text' => $text,
781
			'href' => $title->getLocalURL( $query ),
782
			'primary' => true ];
783
		if ( $linkClass !== '' ) {
784
			$result['link-class'] = $linkClass;
785
		}
786
787
		return $result;
788
	}
789
790
	function makeTalkUrlDetails( $name, $urlaction = '' ) {
791
		$title = Title::newFromText( $name );
792
		if ( !is_object( $title ) ) {
793
			throw new MWException( __METHOD__ . " given invalid pagename $name" );
794
		}
795
		$title = $title->getTalkPage();
796
		self::checkTitle( $title, $name );
797
		return [
798
			'href' => $title->getLocalURL( $urlaction ),
799
			'exists' => $title->isKnown(),
800
		];
801
	}
802
803
	/**
804
	 * @todo is this even used?
805
	 */
806 View Code Duplication
	function makeArticleUrlDetails( $name, $urlaction = '' ) {
807
		$title = Title::newFromText( $name );
808
		$title = $title->getSubjectPage();
809
		self::checkTitle( $title, $name );
810
		return [
811
			'href' => $title->getLocalURL( $urlaction ),
812
			'exists' => $title->exists(),
813
		];
814
	}
815
816
	/**
817
	 * a structured array of links usually used for the tabs in a skin
818
	 *
819
	 * There are 4 standard sections
820
	 * namespaces: Used for namespace tabs like special, page, and talk namespaces
821
	 * views: Used for primary page views like read, edit, history
822
	 * actions: Used for most extra page actions like deletion, protection, etc...
823
	 * variants: Used to list the language variants for the page
824
	 *
825
	 * Each section's value is a key/value array of links for that section.
826
	 * The links themselves have these common keys:
827
	 * - class: The css classes to apply to the tab
828
	 * - text: The text to display on the tab
829
	 * - href: The href for the tab to point to
830
	 * - rel: An optional rel= for the tab's link
831
	 * - redundant: If true the tab will be dropped in skins using content_actions
832
	 *   this is useful for tabs like "Read" which only have meaning in skins that
833
	 *   take special meaning from the grouped structure of content_navigation
834
	 *
835
	 * Views also have an extra key which can be used:
836
	 * - primary: If this is not true skins like vector may try to hide the tab
837
	 *            when the user has limited space in their browser window
838
	 *
839
	 * content_navigation using code also expects these ids to be present on the
840
	 * links, however these are usually automatically generated by SkinTemplate
841
	 * itself and are not necessary when using a hook. The only things these may
842
	 * matter to are people modifying content_navigation after it's initial creation:
843
	 * - id: A "preferred" id, most skins are best off outputting this preferred
844
	 *   id for best compatibility.
845
	 * - tooltiponly: This is set to true for some tabs in cases where the system
846
	 *   believes that the accesskey should not be added to the tab.
847
	 *
848
	 * @return array
849
	 */
850
	protected function buildContentNavigationUrls() {
851
		global $wgDisableLangConversion;
852
853
		// Display tabs for the relevant title rather than always the title itself
854
		$title = $this->getRelevantTitle();
855
		$onPage = $title->equals( $this->getTitle() );
0 ignored issues
show
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...
856
857
		$out = $this->getOutput();
858
		$request = $this->getRequest();
859
		$user = $this->getUser();
860
861
		$content_navigation = [
862
			'namespaces' => [],
863
			'views' => [],
864
			'actions' => [],
865
			'variants' => []
866
		];
867
868
		// parameters
869
		$action = $request->getVal( 'action', 'view' );
870
871
		$userCanRead = $title->quickUserCan( 'read', $user );
872
873
		$preventActiveTabs = false;
874
		Hooks::run( 'SkinTemplatePreventOtherActiveTabs', [ &$this, &$preventActiveTabs ] );
875
876
		// Checks if page is some kind of content
877
		if ( $title->canExist() ) {
878
			// Gets page objects for the related namespaces
879
			$subjectPage = $title->getSubjectPage();
880
			$talkPage = $title->getTalkPage();
881
882
			// Determines if this is a talk page
883
			$isTalk = $title->isTalkPage();
884
885
			// Generates XML IDs from namespace names
886
			$subjectId = $title->getNamespaceKey( '' );
887
888
			if ( $subjectId == 'main' ) {
889
				$talkId = 'talk';
890
			} else {
891
				$talkId = "{$subjectId}_talk";
892
			}
893
894
			$skname = $this->skinname;
895
896
			// Adds namespace links
897
			$subjectMsg = [ "nstab-$subjectId" ];
898
			if ( $subjectPage->isMainPage() ) {
899
				array_unshift( $subjectMsg, 'mainpage-nstab' );
900
			}
901
			$content_navigation['namespaces'][$subjectId] = $this->tabAction(
902
				$subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs, '', $userCanRead
903
			);
904
			$content_navigation['namespaces'][$subjectId]['context'] = 'subject';
905
			$content_navigation['namespaces'][$talkId] = $this->tabAction(
906
				$talkPage, [ "nstab-$talkId", 'talk' ], $isTalk && !$preventActiveTabs, '', $userCanRead
907
			);
908
			$content_navigation['namespaces'][$talkId]['context'] = 'talk';
909
910
			if ( $userCanRead ) {
911
				// Adds "view" view link
912
				if ( $title->isKnown() ) {
913
					$content_navigation['views']['view'] = $this->tabAction(
914
						$isTalk ? $talkPage : $subjectPage,
915
						[ "$skname-view-view", 'view' ],
916
						( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true
917
					);
918
					// signal to hide this from simple content_actions
919
					$content_navigation['views']['view']['redundant'] = true;
920
				}
921
922
				$isForeignFile = $title->inNamespace( NS_FILE ) && $this->canUseWikiPage() &&
923
					$this->getWikiPage() instanceof WikiFilePage && !$this->getWikiPage()->isLocal();
924
925
				// If it is a non-local file, show a link to the file in its own repository
926
				// @todo abstract this for remote content that isn't a file
927
				if ( $isForeignFile ) {
928
					$file = $this->getWikiPage()->getFile();
929
					$content_navigation['views']['view-foreign'] = [
930
						'class' => '',
931
						'text' => wfMessageFallback( "$skname-view-foreign", 'view-foreign' )->
932
							setContext( $this->getContext() )->
933
							params( $file->getRepo()->getDisplayName() )->text(),
934
						'href' => $file->getDescriptionUrl(),
935
						'primary' => false,
936
					];
937
				}
938
939
				// Checks if user can edit the current page if it exists or create it otherwise
940
				if ( $title->quickUserCan( 'edit', $user )
941
					&& ( $title->exists() || $title->quickUserCan( 'create', $user ) )
942
				) {
943
					// Builds CSS class for talk page links
944
					$isTalkClass = $isTalk ? ' istalk' : '';
945
					// Whether the user is editing the page
946
					$isEditing = $onPage && ( $action == 'edit' || $action == 'submit' );
947
					// Whether to show the "Add a new section" tab
948
					// Checks if this is a current rev of talk page and is not forced to be hidden
949
					$showNewSection = !$out->forceHideNewSectionLink()
950
						&& ( ( $isTalk && $this->isRevisionCurrent() ) || $out->showNewSectionLink() );
951
					$section = $request->getVal( 'section' );
952
953
					if ( $title->exists()
954
						|| ( $title->getNamespace() == NS_MEDIAWIKI
955
							&& $title->getDefaultMessageText() !== false
956
						)
957
					) {
958
						$msgKey = $isForeignFile ? 'edit-local' : 'edit';
959
					} else {
960
						$msgKey = $isForeignFile ? 'create-local' : 'create';
961
					}
962
					$content_navigation['views']['edit'] = [
963
						'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection )
964
							? 'selected'
965
							: ''
966
						) . $isTalkClass,
967
						'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey )
968
							->setContext( $this->getContext() )->text(),
969
						'href' => $title->getLocalURL( $this->editUrlOptions() ),
970
						'primary' => !$isForeignFile, // don't collapse this in vector
971
					];
972
973
					// section link
974
					if ( $showNewSection ) {
975
						// Adds new section link
976
						// $content_navigation['actions']['addsection']
977
						$content_navigation['views']['addsection'] = [
978
							'class' => ( $isEditing && $section == 'new' ) ? 'selected' : false,
979
							'text' => wfMessageFallback( "$skname-action-addsection", 'addsection' )
980
								->setContext( $this->getContext() )->text(),
981
							'href' => $title->getLocalURL( 'action=edit&section=new' )
982
						];
983
					}
984
				// Checks if the page has some kind of viewable source content
985 View Code Duplication
				} elseif ( $title->hasSourceText() ) {
986
					// Adds view source view link
987
					$content_navigation['views']['viewsource'] = [
988
						'class' => ( $onPage && $action == 'edit' ) ? 'selected' : false,
989
						'text' => wfMessageFallback( "$skname-action-viewsource", 'viewsource' )
990
							->setContext( $this->getContext() )->text(),
991
						'href' => $title->getLocalURL( $this->editUrlOptions() ),
992
						'primary' => true, // don't collapse this in vector
993
					];
994
				}
995
996
				// Checks if the page exists
997
				if ( $title->exists() ) {
998
					// Adds history view link
999
					$content_navigation['views']['history'] = [
1000
						'class' => ( $onPage && $action == 'history' ) ? 'selected' : false,
1001
						'text' => wfMessageFallback( "$skname-view-history", 'history_short' )
1002
							->setContext( $this->getContext() )->text(),
1003
						'href' => $title->getLocalURL( 'action=history' ),
1004
					];
1005
1006 View Code Duplication
					if ( $title->quickUserCan( 'delete', $user ) ) {
1007
						$content_navigation['actions']['delete'] = [
1008
							'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false,
1009
							'text' => wfMessageFallback( "$skname-action-delete", 'delete' )
1010
								->setContext( $this->getContext() )->text(),
1011
							'href' => $title->getLocalURL( 'action=delete' )
1012
						];
1013
					}
1014
1015
					if ( $title->quickUserCan( 'move', $user ) ) {
1016
						$moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() );
1017
						$content_navigation['actions']['move'] = [
1018
							'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false,
1019
							'text' => wfMessageFallback( "$skname-action-move", 'move' )
1020
								->setContext( $this->getContext() )->text(),
1021
							'href' => $moveTitle->getLocalURL()
1022
						];
1023
					}
1024
				} else {
1025
					// article doesn't exist or is deleted
1026
					if ( $user->isAllowed( 'deletedhistory' ) ) {
1027
						$n = $title->isDeleted();
1028
						if ( $n ) {
1029
							$undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() );
1030
							// If the user can't undelete but can view deleted
1031
							// history show them a "View .. deleted" tab instead.
1032
							$msgKey = $user->isAllowed( 'undelete' ) ? 'undelete' : 'viewdeleted';
1033
							$content_navigation['actions']['undelete'] = [
1034
								'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false,
1035
								'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" )
1036
									->setContext( $this->getContext() )->numParams( $n )->text(),
1037
								'href' => $undelTitle->getLocalURL()
1038
							];
1039
						}
1040
					}
1041
				}
1042
1043
				if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
1044
					MWNamespace::getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
1045
				) {
1046
					$mode = $title->isProtected() ? 'unprotect' : 'protect';
1047
					$content_navigation['actions'][$mode] = [
1048
						'class' => ( $onPage && $action == $mode ) ? 'selected' : false,
1049
						'text' => wfMessageFallback( "$skname-action-$mode", $mode )
1050
							->setContext( $this->getContext() )->text(),
1051
						'href' => $title->getLocalURL( "action=$mode" )
1052
					];
1053
				}
1054
1055
				// Checks if the user is logged in
1056
				if ( $this->loggedin && $user->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) ) {
1057
					/**
1058
					 * The following actions use messages which, if made particular to
1059
					 * the any specific skins, would break the Ajax code which makes this
1060
					 * action happen entirely inline. OutputPage::getJSVars
1061
					 * defines a set of messages in a javascript object - and these
1062
					 * messages are assumed to be global for all skins. Without making
1063
					 * a change to that procedure these messages will have to remain as
1064
					 * the global versions.
1065
					 */
1066
					$mode = $user->isWatched( $title ) ? 'unwatch' : 'watch';
0 ignored issues
show
It seems like $title defined by $this->getRelevantTitle() on line 854 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...
1067
					$content_navigation['actions'][$mode] = [
1068
						'class' => 'mw-watchlink ' . (
1069
							$onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : ''
1070
						),
1071
						// uses 'watch' or 'unwatch' message
1072
						'text' => $this->msg( $mode )->text(),
1073
						'href' => $title->getLocalURL( [ 'action' => $mode ] )
1074
					];
1075
				}
1076
			}
1077
1078
			Hooks::run( 'SkinTemplateNavigation', [ &$this, &$content_navigation ] );
1079
1080
			if ( $userCanRead && !$wgDisableLangConversion ) {
1081
				$pageLang = $title->getPageLanguage();
1082
				// Gets list of language variants
1083
				$variants = $pageLang->getVariants();
1084
				// Checks that language conversion is enabled and variants exist
1085
				// And if it is not in the special namespace
1086
				if ( count( $variants ) > 1 ) {
1087
					// Gets preferred variant (note that user preference is
1088
					// only possible for wiki content language variant)
1089
					$preferred = $pageLang->getPreferredVariant();
1090
					if ( Action::getActionName( $this ) === 'view' ) {
1091
						$params = $request->getQueryValues();
1092
						unset( $params['title'] );
1093
					} else {
1094
						$params = [];
1095
					}
1096
					// Loops over each variant
1097
					foreach ( $variants as $code ) {
1098
						// Gets variant name from language code
1099
						$varname = $pageLang->getVariantname( $code );
1100
						// Appends variant link
1101
						$content_navigation['variants'][] = [
1102
							'class' => ( $code == $preferred ) ? 'selected' : false,
1103
							'text' => $varname,
1104
							'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ),
1105
							'lang' => wfBCP47( $code ),
1106
							'hreflang' => wfBCP47( $code ),
1107
						];
1108
					}
1109
				}
1110
			}
1111
		} else {
1112
			// If it's not content, it's got to be a special page
1113
			$content_navigation['namespaces']['special'] = [
1114
				'class' => 'selected',
1115
				'text' => $this->msg( 'nstab-special' )->text(),
1116
				'href' => $request->getRequestURL(), // @see: bug 2457, bug 2510
1117
				'context' => 'subject'
1118
			];
1119
1120
			Hooks::run( 'SkinTemplateNavigation::SpecialPage',
1121
				[ &$this, &$content_navigation ] );
1122
		}
1123
1124
		// Equiv to SkinTemplateContentActions
1125
		Hooks::run( 'SkinTemplateNavigation::Universal', [ &$this, &$content_navigation ] );
1126
1127
		// Setup xml ids and tooltip info
1128
		foreach ( $content_navigation as $section => &$links ) {
1129
			foreach ( $links as $key => &$link ) {
1130
				$xmlID = $key;
1131
				if ( isset( $link['context'] ) && $link['context'] == 'subject' ) {
1132
					$xmlID = 'ca-nstab-' . $xmlID;
1133
				} elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) {
1134
					$xmlID = 'ca-talk';
1135
					$link['rel'] = 'discussion';
1136
				} elseif ( $section == 'variants' ) {
1137
					$xmlID = 'ca-varlang-' . $xmlID;
1138
				} else {
1139
					$xmlID = 'ca-' . $xmlID;
1140
				}
1141
				$link['id'] = $xmlID;
1142
			}
1143
		}
1144
1145
		# We don't want to give the watch tab an accesskey if the
1146
		# page is being edited, because that conflicts with the
1147
		# accesskey on the watch checkbox.  We also don't want to
1148
		# give the edit tab an accesskey, because that's fairly
1149
		# superfluous and conflicts with an accesskey (Ctrl-E) often
1150
		# used for editing in Safari.
1151
		if ( in_array( $action, [ 'edit', 'submit' ] ) ) {
1152
			if ( isset( $content_navigation['views']['edit'] ) ) {
1153
				$content_navigation['views']['edit']['tooltiponly'] = true;
1154
			}
1155 View Code Duplication
			if ( isset( $content_navigation['actions']['watch'] ) ) {
1156
				$content_navigation['actions']['watch']['tooltiponly'] = true;
1157
			}
1158 View Code Duplication
			if ( isset( $content_navigation['actions']['unwatch'] ) ) {
1159
				$content_navigation['actions']['unwatch']['tooltiponly'] = true;
1160
			}
1161
		}
1162
1163
		return $content_navigation;
1164
	}
1165
1166
	/**
1167
	 * an array of edit links by default used for the tabs
1168
	 * @param array $content_navigation
1169
	 * @return array
1170
	 */
1171
	private function buildContentActionUrls( $content_navigation ) {
1172
		// content_actions has been replaced with content_navigation for backwards
1173
		// compatibility and also for skins that just want simple tabs content_actions
1174
		// is now built by flattening the content_navigation arrays into one
1175
1176
		$content_actions = [];
1177
1178
		foreach ( $content_navigation as $links ) {
1179
			foreach ( $links as $key => $value ) {
1180
				if ( isset( $value['redundant'] ) && $value['redundant'] ) {
1181
					// Redundant tabs are dropped from content_actions
1182
					continue;
1183
				}
1184
1185
				// content_actions used to have ids built using the "ca-$key" pattern
1186
				// so the xmlID based id is much closer to the actual $key that we want
1187
				// for that reason we'll just strip out the ca- if present and use
1188
				// the latter potion of the "id" as the $key
1189
				if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) {
1190
					$key = substr( $value['id'], 3 );
1191
				}
1192
1193
				if ( isset( $content_actions[$key] ) ) {
1194
					wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " .
1195
						"content_navigation into content_actions.\n" );
1196
					continue;
1197
				}
1198
1199
				$content_actions[$key] = $value;
1200
			}
1201
		}
1202
1203
		return $content_actions;
1204
	}
1205
1206
	/**
1207
	 * build array of common navigation links
1208
	 * @return array
1209
	 */
1210
	protected function buildNavUrls() {
1211
		global $wgUploadNavigationUrl;
1212
1213
		$out = $this->getOutput();
1214
		$request = $this->getRequest();
1215
1216
		$nav_urls = [];
1217
		$nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1218
		if ( $wgUploadNavigationUrl ) {
1219
			$nav_urls['upload'] = [ 'href' => $wgUploadNavigationUrl ];
1220
		} elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) {
1221
			$nav_urls['upload'] = [ 'href' => self::makeSpecialUrl( 'Upload' ) ];
1222
		} else {
1223
			$nav_urls['upload'] = false;
1224
		}
1225
		$nav_urls['specialpages'] = [ 'href' => self::makeSpecialUrl( 'Specialpages' ) ];
1226
1227
		$nav_urls['print'] = false;
1228
		$nav_urls['permalink'] = false;
1229
		$nav_urls['info'] = false;
1230
		$nav_urls['whatlinkshere'] = false;
1231
		$nav_urls['recentchangeslinked'] = false;
1232
		$nav_urls['contributions'] = false;
1233
		$nav_urls['log'] = false;
1234
		$nav_urls['blockip'] = false;
1235
		$nav_urls['emailuser'] = false;
1236
		$nav_urls['userrights'] = false;
1237
1238
		// A print stylesheet is attached to all pages, but nobody ever
1239
		// figures that out. :)  Add a link...
1240
		if ( !$out->isPrintable() && ( $out->isArticle() || $this->getTitle()->isSpecialPage() ) ) {
1241
			$nav_urls['print'] = [
1242
				'text' => $this->msg( 'printableversion' )->text(),
1243
				'href' => $this->getTitle()->getLocalURL(
1244
					$request->appendQueryValue( 'printable', 'yes' ) )
1245
			];
1246
		}
1247
1248
		if ( $out->isArticle() ) {
1249
			// Also add a "permalink" while we're at it
1250
			$revid = $this->getRevisionId();
1251
			if ( $revid ) {
1252
				$nav_urls['permalink'] = [
1253
					'text' => $this->msg( 'permalink' )->text(),
1254
					'href' => $this->getTitle()->getLocalURL( "oldid=$revid" )
1255
				];
1256
			}
1257
1258
			// Use the copy of revision ID in case this undocumented, shady hook tries to mess with internals
1259
			Hooks::run( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink',
1260
				[ &$this, &$nav_urls, &$revid, &$revid ] );
1261
		}
1262
1263
		if ( $out->isArticleRelated() ) {
1264
			$nav_urls['whatlinkshere'] = [
1265
				'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalURL()
1266
			];
1267
1268
			$nav_urls['info'] = [
1269
				'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1270
				'href' => $this->getTitle()->getLocalURL( "action=info" )
1271
			];
1272
1273
			if ( $this->getTitle()->exists() ) {
1274
				$nav_urls['recentchangeslinked'] = [
1275
					'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalURL()
1276
				];
1277
			}
1278
		}
1279
1280
		$user = $this->getRelevantUser();
1281
		if ( $user ) {
1282
			$rootUser = $user->getName();
1283
1284
			$nav_urls['contributions'] = [
1285
				'text' => $this->msg( 'contributions', $rootUser )->text(),
1286
				'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1287
				'tooltip-params' => [ $rootUser ],
1288
			];
1289
1290
			$nav_urls['log'] = [
1291
				'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
1292
			];
1293
1294
			if ( $this->getUser()->isAllowed( 'block' ) ) {
1295
				$nav_urls['blockip'] = [
1296
					'text' => $this->msg( 'blockip', $rootUser )->text(),
1297
					'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser )
1298
				];
1299
			}
1300
1301
			if ( $this->showEmailUser( $user ) ) {
1302
				$nav_urls['emailuser'] = [
1303
					'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1304
					'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1305
					'tooltip-params' => [ $rootUser ],
1306
				];
1307
			}
1308
1309
			if ( !$user->isAnon() ) {
1310
				$sur = new UserrightsPage;
1311
				$sur->setContext( $this->getContext() );
1312
				if ( $sur->userCanExecute( $this->getUser() ) ) {
1313
					$nav_urls['userrights'] = [
1314
						'text' => $this->msg( 'tool-link-userrights', $this->getUser()->getName() )->text(),
1315
						'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
1316
					];
1317
				}
1318
			}
1319
		}
1320
1321
		return $nav_urls;
1322
	}
1323
1324
	/**
1325
	 * Generate strings used for xml 'id' names
1326
	 * @return string
1327
	 */
1328
	protected function getNameSpaceKey() {
1329
		return $this->getTitle()->getNamespaceKey();
1330
	}
1331
}
1332