Completed
Branch master (8ef871)
by
unknown
29:40
created

SkinTemplate::setupTemplateForOutput()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 32
Code Lines 22

Duplication

Lines 6
Ratio 18.75 %
Metric Value
dl 6
loc 32
rs 8.8571
cc 3
eloc 22
nc 4
nop 0
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
	/**
49
	 * Add specific styles for this skin
50
	 *
51
	 * @param OutputPage $out
52
	 */
53
	function setupSkinUserCss( OutputPage $out ) {
54
		$moduleStyles = [
55
			'mediawiki.legacy.shared',
56
			'mediawiki.legacy.commonPrint',
57
			'mediawiki.sectionAnchor'
58
		];
59
		if ( $out->isSyndicated() ) {
60
			$moduleStyles[] = 'mediawiki.feedlink';
61
		}
62
63
		// Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
64
		// on every page is deprecated. Express a dependency instead.
65
		if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
66
			$moduleStyles[] = 'mediawiki.ui.button';
67
		}
68
69
		$out->addModuleStyles( $moduleStyles );
70
	}
71
72
	/**
73
	 * Create the template engine object; we feed it a bunch of data
74
	 * and eventually it spits out some HTML. Should have interface
75
	 * roughly equivalent to PHPTAL 0.7.
76
	 *
77
	 * @param string $classname
78
	 * @param bool|string $repository Subdirectory where we keep template files
79
	 * @param bool|string $cache_dir
80
	 * @return QuickTemplate
81
	 * @private
82
	 */
83
	function setupTemplate( $classname, $repository = false, $cache_dir = false ) {
84
		return new $classname( $this->getConfig() );
85
	}
86
87
	/**
88
	 * Generates array of language links for the current page
89
	 *
90
	 * @return array
91
	 */
92
	public function getLanguages() {
93
		global $wgHideInterlanguageLinks;
94
		if ( $wgHideInterlanguageLinks ) {
95
			return [];
96
		}
97
98
		$userLang = $this->getLanguage();
99
		$languageLinks = [];
100
101
		foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
102
			$class = 'interlanguage-link interwiki-' . explode( ':', $languageLinkText, 2 )[0];
103
104
			$languageLinkTitle = Title::newFromText( $languageLinkText );
105
			if ( $languageLinkTitle ) {
106
				$ilInterwikiCode = $languageLinkTitle->getInterwiki();
107
				$ilLangName = Language::fetchLanguageName( $ilInterwikiCode );
108
109
				if ( strval( $ilLangName ) === '' ) {
110
					$ilDisplayTextMsg = wfMessage( "interlanguage-link-$ilInterwikiCode" );
111
					if ( !$ilDisplayTextMsg->isDisabled() ) {
112
						// Use custom MW message for the display text
113
						$ilLangName = $ilDisplayTextMsg->text();
114
					} else {
115
						// Last resort: fallback to the language link target
116
						$ilLangName = $languageLinkText;
117
					}
118
				} else {
119
					// Use the language autonym as display text
120
					$ilLangName = $this->formatLanguageName( $ilLangName );
121
				}
122
123
				// CLDR extension or similar is required to localize the language name;
124
				// otherwise we'll end up with the autonym again.
125
				$ilLangLocalName = Language::fetchLanguageName(
126
					$ilInterwikiCode,
127
					$userLang->getCode()
128
				);
129
130
				$languageLinkTitleText = $languageLinkTitle->getText();
131
				if ( $ilLangLocalName === '' ) {
132
					$ilFriendlySiteName = wfMessage( "interlanguage-link-sitename-$ilInterwikiCode" );
133
					if ( !$ilFriendlySiteName->isDisabled() ) {
134
						if ( $languageLinkTitleText === '' ) {
135
							$ilTitle = wfMessage(
136
								'interlanguage-link-title-nonlangonly',
137
								$ilFriendlySiteName->text()
138
							)->text();
139
						} else {
140
							$ilTitle = wfMessage(
141
								'interlanguage-link-title-nonlang',
142
								$languageLinkTitleText,
143
								$ilFriendlySiteName->text()
144
							)->text();
145
						}
146
					} else {
147
						// we have nothing friendly to put in the title, so fall back to
148
						// displaying the interlanguage link itself in the title text
149
						// (similar to what is done in page content)
150
						$ilTitle = $languageLinkTitle->getInterwiki() .
151
							":$languageLinkTitleText";
152
					}
153
				} elseif ( $languageLinkTitleText === '' ) {
154
					$ilTitle = wfMessage(
155
						'interlanguage-link-title-langonly',
156
						$ilLangLocalName
157
					)->text();
158
				} else {
159
					$ilTitle = wfMessage(
160
						'interlanguage-link-title',
161
						$languageLinkTitleText,
162
						$ilLangLocalName
163
					)->text();
164
				}
165
166
				$ilInterwikiCodeBCP47 = wfBCP47( $ilInterwikiCode );
167
				$languageLink = [
168
					'href' => $languageLinkTitle->getFullURL(),
169
					'text' => $ilLangName,
170
					'title' => $ilTitle,
171
					'class' => $class,
172
					'lang' => $ilInterwikiCodeBCP47,
173
					'hreflang' => $ilInterwikiCodeBCP47,
174
				];
175
				Hooks::run(
176
					'SkinTemplateGetLanguageLink',
177
					[ &$languageLink, $languageLinkTitle, $this->getTitle(), $this->getOutput() ]
178
				);
179
				$languageLinks[] = $languageLink;
180
			}
181
		}
182
183
		return $languageLinks;
184
	}
185
186
	protected function setupTemplateForOutput() {
187
188
		$request = $this->getRequest();
189
		$user = $this->getUser();
190
		$title = $this->getTitle();
191
192
		$tpl = $this->setupTemplate( $this->template, 'skins' );
193
194
		$this->thispage = $title->getPrefixedDBkey();
0 ignored issues
show
Bug introduced by
The property thispage does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
195
		$this->titletxt = $title->getPrefixedText();
0 ignored issues
show
Bug introduced by
The property titletxt does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
196
		$this->userpage = $user->getUserPage()->getPrefixedText();
0 ignored issues
show
Bug introduced by
The property userpage does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
197
		$query = [];
198 View Code Duplication
		if ( !$request->wasPosted() ) {
199
			$query = $request->getValues();
200
			unset( $query['title'] );
201
			unset( $query['returnto'] );
202
			unset( $query['returntoquery'] );
203
		}
204
		$this->thisquery = wfArrayToCgi( $query );
0 ignored issues
show
Bug introduced by
The property thisquery does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
205
		$this->loggedin = $user->isLoggedIn();
0 ignored issues
show
Bug introduced by
The property loggedin does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
206
		$this->username = $user->getName();
0 ignored issues
show
Bug introduced by
The property username does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
207
208
		if ( $this->loggedin ) {
209
			$this->userpageUrlDetails = self::makeUrlDetails( $this->userpage );
0 ignored issues
show
Bug introduced by
The property userpageUrlDetails does not seem to exist. Did you mean userpage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
210
		} else {
211
			# This won't be used in the standard skins, but we define it to preserve the interface
212
			# To save time, we check for existence
213
			$this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage );
0 ignored issues
show
Bug introduced by
The property userpageUrlDetails does not seem to exist. Did you mean userpage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
214
		}
215
216
		return $tpl;
217
	}
218
219
	/**
220
	 * initialize various variables and generate the template
221
	 *
222
	 * @param OutputPage $out
223
	 */
224
	function outputPage( OutputPage $out = null ) {
225
		Profiler::instance()->setTemplated( true );
226
227
		$oldContext = null;
228
		if ( $out !== null ) {
229
			// Deprecated since 1.20, note added in 1.25
230
			wfDeprecated( __METHOD__, '1.25' );
231
			$oldContext = $this->getContext();
232
			$this->setContext( $out->getContext() );
233
		}
234
235
		$out = $this->getOutput();
236
237
		$this->initPage( $out );
238
		$tpl = $this->prepareQuickTemplate( $out );
239
		// execute template
240
		$res = $tpl->execute();
241
242
		// result may be an error
243
		$this->printOrError( $res );
244
245
		if ( $oldContext ) {
246
			$this->setContext( $oldContext );
247
		}
248
249
	}
250
251
	/**
252
	 * Wrap the body text with language information and identifiable element
253
	 *
254
	 * @param Title $title
255
	 * @return string html
256
	 */
257
	protected function wrapHTML( $title, $html ) {
258
		# An ID that includes the actual body text; without categories, contentSub, ...
259
		$realBodyAttribs = [ 'id' => 'mw-content-text' ];
260
261
		# Add a mw-content-ltr/rtl class to be able to style based on text direction
262
		# when the content is different from the UI language, i.e.:
263
		# not for special pages or file pages AND only when viewing
264
		if ( !in_array( $title->getNamespace(), [ NS_SPECIAL, NS_FILE ] ) &&
265
			Action::getActionName( $this ) === 'view' ) {
266
			$pageLang = $title->getPageViewLanguage();
267
			$realBodyAttribs['lang'] = $pageLang->getHtmlCode();
268
			$realBodyAttribs['dir'] = $pageLang->getDir();
269
			$realBodyAttribs['class'] = 'mw-content-' . $pageLang->getDir();
270
		}
271
272
		return Html::rawElement( 'div', $realBodyAttribs, $html );
273
	}
274
275
	/**
276
	 * initialize various variables and generate the template
277
	 *
278
	 * @since 1.23
279
	 * @return QuickTemplate The template to be executed by outputPage
280
	 */
281
	protected function prepareQuickTemplate() {
282
		global $wgContLang, $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType,
283
			$wgSitename, $wgLogo, $wgMaxCredits,
284
			$wgShowCreditsIfMax, $wgArticlePath,
285
			$wgScriptPath, $wgServer;
286
287
		$title = $this->getTitle();
288
		$request = $this->getRequest();
289
		$out = $this->getOutput();
290
		$tpl = $this->setupTemplateForOutput();
291
292
		$tpl->set( 'title', $out->getPageTitle() );
293
		$tpl->set( 'pagetitle', $out->getHTMLTitle() );
294
		$tpl->set( 'displaytitle', $out->mPageLinkTitle );
295
296
		$tpl->setRef( 'thispage', $this->thispage );
297
		$tpl->setRef( 'titleprefixeddbkey', $this->thispage );
298
		$tpl->set( 'titletext', $title->getText() );
299
		$tpl->set( 'articleid', $title->getArticleID() );
300
301
		$tpl->set( 'isarticle', $out->isArticle() );
302
303
		$subpagestr = $this->subPageSubtitle();
304
		if ( $subpagestr !== '' ) {
305
			$subpagestr = '<span class="subpages">' . $subpagestr . '</span>';
306
		}
307
		$tpl->set( 'subtitle', $subpagestr . $out->getSubtitle() );
308
309
		$undelete = $this->getUndeleteLink();
310
		if ( $undelete === '' ) {
311
			$tpl->set( 'undelete', '' );
312
		} else {
313
			$tpl->set( 'undelete', '<span class="subpages">' . $undelete . '</span>' );
314
		}
315
316
		$tpl->set( 'catlinks', $this->getCategories() );
317
		if ( $out->isSyndicated() ) {
318
			$feeds = [];
319
			foreach ( $out->getSyndicationLinks() as $format => $link ) {
320
				$feeds[$format] = [
321
					// Messages: feed-atom, feed-rss
322
					'text' => $this->msg( "feed-$format" )->text(),
323
					'href' => $link
324
				];
325
			}
326
			$tpl->setRef( 'feeds', $feeds );
327
		} else {
328
			$tpl->set( 'feeds', false );
329
		}
330
331
		$tpl->setRef( 'mimetype', $wgMimeType );
332
		$tpl->setRef( 'jsmimetype', $wgJsMimeType );
333
		$tpl->set( 'charset', 'UTF-8' );
334
		$tpl->setRef( 'wgScript', $wgScript );
335
		$tpl->setRef( 'skinname', $this->skinname );
336
		$tpl->set( 'skinclass', get_class( $this ) );
337
		$tpl->setRef( 'skin', $this );
338
		$tpl->setRef( 'stylename', $this->stylename );
339
		$tpl->set( 'printable', $out->isPrintable() );
340
		$tpl->set( 'handheld', $request->getBool( 'handheld' ) );
341
		$tpl->setRef( 'loggedin', $this->loggedin );
342
		$tpl->set( 'notspecialpage', !$title->isSpecialPage() );
343
		$tpl->set( 'searchaction', $this->escapeSearchLink() );
344
		$tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey() );
345
		$tpl->set( 'search', trim( $request->getVal( 'search' ) ) );
346
		$tpl->setRef( 'stylepath', $wgStylePath );
347
		$tpl->setRef( 'articlepath', $wgArticlePath );
348
		$tpl->setRef( 'scriptpath', $wgScriptPath );
349
		$tpl->setRef( 'serverurl', $wgServer );
350
		$tpl->setRef( 'logopath', $wgLogo );
351
		$tpl->setRef( 'sitename', $wgSitename );
352
353
		$userLang = $this->getLanguage();
354
		$userLangCode = $userLang->getHtmlCode();
355
		$userLangDir = $userLang->getDir();
356
357
		$tpl->set( 'lang', $userLangCode );
358
		$tpl->set( 'dir', $userLangDir );
359
		$tpl->set( 'rtl', $userLang->isRTL() );
360
361
		$tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
362
		$tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed
363
		$tpl->set( 'username', $this->loggedin ? $this->username : null );
364
		$tpl->setRef( 'userpage', $this->userpage );
365
		$tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] );
0 ignored issues
show
Bug introduced by
The property userpageUrlDetails does not seem to exist. Did you mean userpage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
366
		$tpl->set( 'userlang', $userLangCode );
367
368
		// Users can have their language set differently than the
369
		// content of the wiki. For these users, tell the web browser
370
		// that interface elements are in a different language.
371
		$tpl->set( 'userlangattributes', '' );
372
		$tpl->set( 'specialpageattributes', '' ); # obsolete
373
		// Used by VectorBeta to insert HTML before content but after the
374
		// heading for the page title. Defaults to empty string.
375
		$tpl->set( 'prebodyhtml', '' );
376
377
		if ( $userLangCode !== $wgContLang->getHtmlCode() || $userLangDir !== $wgContLang->getDir() ) {
378
			$escUserlang = htmlspecialchars( $userLangCode );
379
			$escUserdir = htmlspecialchars( $userLangDir );
380
			// Attributes must be in double quotes because htmlspecialchars() doesn't
381
			// escape single quotes
382
			$attrs = " lang=\"$escUserlang\" dir=\"$escUserdir\"";
383
			$tpl->set( 'userlangattributes', $attrs );
384
		}
385
386
		$tpl->set( 'newtalk', $this->getNewtalks() );
387
		$tpl->set( 'logo', $this->logoText() );
388
389
		$tpl->set( 'copyright', false );
390
		// No longer used
391
		$tpl->set( 'viewcount', false );
392
		$tpl->set( 'lastmod', false );
393
		$tpl->set( 'credits', false );
394
		$tpl->set( 'numberofwatchingusers', false );
395
		if ( $out->isArticle() && $title->exists() ) {
396
			if ( $this->isRevisionCurrent() ) {
397
				if ( $wgMaxCredits != 0 ) {
398
					$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...
399
						$this->getContext() )->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) );
400
				} else {
401
					$tpl->set( 'lastmod', $this->lastModified() );
402
				}
403
			}
404
			$tpl->set( 'copyright', $this->getCopyright() );
405
		}
406
407
		$tpl->set( 'copyrightico', $this->getCopyrightIcon() );
408
		$tpl->set( 'poweredbyico', $this->getPoweredBy() );
409
		$tpl->set( 'disclaimer', $this->disclaimerLink() );
410
		$tpl->set( 'privacy', $this->privacyLink() );
411
		$tpl->set( 'about', $this->aboutLink() );
412
413
		$tpl->set( 'footerlinks', [
414
			'info' => [
415
				'lastmod',
416
				'numberofwatchingusers',
417
				'credits',
418
				'copyright',
419
			],
420
			'places' => [
421
				'privacy',
422
				'about',
423
				'disclaimer',
424
			],
425
		] );
426
427
		global $wgFooterIcons;
428
		$tpl->set( 'footericons', $wgFooterIcons );
429
		foreach ( $tpl->data['footericons'] as $footerIconsKey => &$footerIconsBlock ) {
430
			if ( count( $footerIconsBlock ) > 0 ) {
431
				foreach ( $footerIconsBlock as &$footerIcon ) {
432
					if ( isset( $footerIcon['src'] ) ) {
433
						if ( !isset( $footerIcon['width'] ) ) {
434
							$footerIcon['width'] = 88;
435
						}
436
						if ( !isset( $footerIcon['height'] ) ) {
437
							$footerIcon['height'] = 31;
438
						}
439
					}
440
				}
441
			} else {
442
				unset( $tpl->data['footericons'][$footerIconsKey] );
443
			}
444
		}
445
446
		$tpl->set( 'indicators', $out->getIndicators() );
447
448
		$tpl->set( 'sitenotice', $this->getSiteNotice() );
449
		$tpl->set( 'bottomscripts', $this->bottomScripts() );
450
		$tpl->set( 'printfooter', $this->printSource() );
451
		// Wrap the bodyText with #mw-content-text element
452
		$out->mBodytext = $this->wrapHTML( $title, $out->mBodytext );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 287 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...
453
		$tpl->setRef( 'bodytext', $out->mBodytext );
454
455
		$language_urls = $this->getLanguages();
456
		if ( count( $language_urls ) ) {
457
			$tpl->setRef( 'language_urls', $language_urls );
458
		} else {
459
			$tpl->set( 'language_urls', false );
460
		}
461
462
		# Personal toolbar
463
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
464
		$content_navigation = $this->buildContentNavigationUrls();
465
		$content_actions = $this->buildContentActionUrls( $content_navigation );
466
		$tpl->setRef( 'content_navigation', $content_navigation );
467
		$tpl->setRef( 'content_actions', $content_actions );
468
469
		$tpl->set( 'sidebar', $this->buildSidebar() );
470
		$tpl->set( 'nav_urls', $this->buildNavUrls() );
471
472
		// Set the head scripts near the end, in case the above actions resulted in added scripts
473
		$tpl->set( 'headelement', $out->headElement( $this ) );
474
475
		$tpl->set( 'debug', '' );
476
		$tpl->set( 'debughtml', $this->generateDebugHTML() );
477
		$tpl->set( 'reporttime', wfReportTime() );
478
479
		// original version by hansm
480
		if ( !Hooks::run( 'SkinTemplateOutputPageBeforeExec', [ &$this, &$tpl ] ) ) {
481
			wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" );
482
		}
483
484
		// Set the bodytext to another key so that skins can just output it on its own
485
		// and output printfooter and debughtml separately
486
		$tpl->set( 'bodycontent', $tpl->data['bodytext'] );
487
488
		// Append printfooter and debughtml onto bodytext so that skins that
489
		// were already using bodytext before they were split out don't suddenly
490
		// start not outputting information.
491
		$tpl->data['bodytext'] .= Html::rawElement(
492
			'div',
493
			[ 'class' => 'printfooter' ],
494
			"\n{$tpl->data['printfooter']}"
495
		) . "\n";
496
		$tpl->data['bodytext'] .= $tpl->data['debughtml'];
497
498
		// allow extensions adding stuff after the page content.
499
		// See Skin::afterContentHook() for further documentation.
500
		$tpl->set( 'dataAfterContent', $this->afterContentHook() );
501
502
		return $tpl;
503
	}
504
505
	/**
506
	 * Get the HTML for the p-personal list
507
	 * @return string
508
	 */
509
	public function getPersonalToolsList() {
510
		$tpl = $this->setupTemplateForOutput();
511
		$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
512
		$html = '';
513
		foreach ( $tpl->getPersonalTools() as $key => $item ) {
514
			$html .= $tpl->makeListItem( $key, $item );
515
		}
516
		return $html;
517
	}
518
519
	/**
520
	 * Format language name for use in sidebar interlanguage links list.
521
	 * By default it is capitalized.
522
	 *
523
	 * @param string $name Language name, e.g. "English" or "español"
524
	 * @return string
525
	 * @private
526
	 */
527
	function formatLanguageName( $name ) {
528
		return $this->getLanguage()->ucfirst( $name );
529
	}
530
531
	/**
532
	 * Output the string, or print error message if it's
533
	 * an error object of the appropriate type.
534
	 * For the base class, assume strings all around.
535
	 *
536
	 * @param string $str
537
	 * @private
538
	 */
539
	function printOrError( $str ) {
540
		echo $str;
541
	}
542
543
	/**
544
	 * Output a boolean indicating if buildPersonalUrls should output separate
545
	 * login and create account links or output a combined link
546
	 * By default we simply return a global config setting that affects most skins
547
	 * This is setup as a method so that like with $wgLogo and getLogo() a skin
548
	 * can override this setting and always output one or the other if it has
549
	 * a reason it can't output one of the two modes.
550
	 * @return bool
551
	 */
552
	function useCombinedLoginLink() {
553
		global $wgUseCombinedLoginLink;
554
		return $wgUseCombinedLoginLink;
555
	}
556
557
	/**
558
	 * build array of urls for personal toolbar
559
	 * @return array
560
	 */
561
	protected function buildPersonalUrls() {
562
		$title = $this->getTitle();
563
		$request = $this->getRequest();
564
		$pageurl = $title->getLocalURL();
565
566
		/* set up the default links for the personal toolbar */
567
		$personal_urls = [];
568
569
		# Due to bug 32276, if a user does not have read permissions,
570
		# $this->getTitle() will just give Special:Badtitle, which is
571
		# not especially useful as a returnto parameter. Use the title
572
		# from the request instead, if there was one.
573
		if ( $this->getUser()->isAllowed( 'read' ) ) {
574
			$page = $this->getTitle();
575
		} else {
576
			$page = Title::newFromText( $request->getVal( 'title', '' ) );
577
		}
578
		$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...
579
		$a = [];
580
		if ( strval( $page ) !== '' ) {
581
			$a['returnto'] = $page;
582
			$query = $request->getVal( 'returntoquery', $this->thisquery );
583
			if ( $query != '' ) {
584
				$a['returntoquery'] = $query;
585
			}
586
		}
587
588
		$returnto = wfArrayToCgi( $a );
589
		if ( $this->loggedin ) {
590
			$personal_urls['userpage'] = [
591
				'text' => $this->username,
592
				'href' => &$this->userpageUrlDetails['href'],
0 ignored issues
show
Bug introduced by
The property userpageUrlDetails does not seem to exist. Did you mean userpage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
593
				'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
0 ignored issues
show
Bug introduced by
The property userpageUrlDetails does not seem to exist. Did you mean userpage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
594
				'active' => ( $this->userpageUrlDetails['href'] == $pageurl ),
0 ignored issues
show
Bug introduced by
The property userpageUrlDetails does not seem to exist. Did you mean userpage?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

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