Completed
Branch master (9259dd)
by
unknown
27:26
created

Skin   F

Complexity

Total Complexity 205

Size/Duplication

Total Lines 1585
Duplicated Lines 1.14 %

Coupling/Cohesion

Components 4
Dependencies 26

Importance

Changes 0
Metric Value
dl 18
loc 1585
rs 0.5217
c 0
b 0
f 0
wmc 205
lcom 4
cbo 26

71 Methods

Rating   Name   Duplication   Size   Complexity  
A getSkinNames() 0 3 1
A getSkinNameMessages() 0 7 2
A getAllowedSkins() 0 11 2
C normalizeKey() 0 40 7
A getSkinName() 0 3 1
A initPage() 0 5 1
D getDefaultModules() 0 42 10
B preloadExistence() 0 29 5
A getRevisionId() 0 3 1
A isRevisionCurrent() 0 4 2
A setRelevantTitle() 0 3 1
A getRelevantTitle() 0 6 2
A setRelevantUser() 0 3 1
B getRelevantUser() 0 24 6
outputPage() 0 1 ?
A makeVariablesScript() 0 9 2
A getDynamicStylesheetQuery() 0 11 1
setupSkinUserCss() 0 1 ?
B getPageClasses() 0 23 4
A getHtmlElementAttributes() 0 8 1
A addToBodyAttributes() 0 3 1
A getLogo() 0 4 1
C getCategoryLinks() 0 62 8
A drawCategoryBrowser() 0 19 3
B getCategories() 0 20 5
A afterContentHook() 0 20 3
A generateDebugHTML() 0 3 1
A bottomScripts() 0 9 1
A printSource() 0 14 2
B getUndeleteLink() 0 24 6
C subPageSubtitle() 0 49 9
A showIPinHeader() 0 4 1
A getSearchLink() 0 4 1
A escapeSearchLink() 0 3 1
C getCopyright() 0 42 8
B getCopyrightIcon() 0 25 5
A getPoweredBy() 0 18 1
B lastModified() 0 22 4
A logoText() 0 16 3
B makeFooterIcon() 0 18 6
A mainPageLink() 0 8 1
A footerLink() 0 21 3
A privacyLink() 0 3 1
A aboutLink() 0 3 1
A disclaimerLink() 0 3 1
A editUrlOptions() 0 9 2
A showEmailUser() 0 12 3
A getSkinStylePath() 0 10 2
A makeMainPageUrl() 0 6 1
A makeSpecialUrl() 0 8 2
A makeSpecialUrlSubpage() 0 4 1
A makeI18nUrl() 0 5 1
A makeUrl() 0 6 1
A makeInternalOrExternalUrl() 0 7 2
A makeNSUrl() 0 6 1
A makeUrlDetails() 9 9 1
A makeKnownUrlDetails() 9 9 1
A checkTitle() 0 8 3
B buildSidebar() 0 31 3
A addToSidebar() 0 3 1
D addToSidebarPlain() 0 84 15
D getNewtalks() 0 86 15
C getCachedNotice() 0 45 8
B getSiteNotice() 0 22 6
A doEditSectionLink() 0 55 3
A commentBlock() 0 4 1
A generateRollback() 0 8 1
A link() 0 4 1
A linkKnown() 0 10 1
A userLink() 0 4 1
A userToolLinks() 0 10 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
/**
3
 * Base class for all skins.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * @defgroup Skins Skins
25
 */
26
27
/**
28
 * The main skin class which provides methods and properties for all other skins.
29
 *
30
 * See docs/skin.txt for more information.
31
 *
32
 * @ingroup Skins
33
 */
34
abstract class Skin extends ContextSource {
35
	protected $skinname = null;
36
	protected $mRelevantTitle = null;
37
	protected $mRelevantUser = null;
38
39
	/**
40
	 * @var string Stylesheets set to use. Subdirectory in skins/ where various stylesheets are
41
	 *   located. Only needs to be set if you intend to use the getSkinStylePath() method.
42
	 */
43
	public $stylename = null;
44
45
	/**
46
	 * Fetch the set of available skins.
47
	 * @return array Associative array of strings
48
	 */
49
	static function getSkinNames() {
50
		return SkinFactory::getDefaultInstance()->getSkinNames();
0 ignored issues
show
Deprecated Code introduced by
The method SkinFactory::getDefaultInstance() has been deprecated with message: in 1.27

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
51
	}
52
53
	/**
54
	 * Fetch the skinname messages for available skins.
55
	 * @return string[]
56
	 */
57
	static function getSkinNameMessages() {
58
		$messages = [];
59
		foreach ( self::getSkinNames() as $skinKey => $skinName ) {
60
			$messages[] = "skinname-$skinKey";
61
		}
62
		return $messages;
63
	}
64
65
	/**
66
	 * Fetch the list of user-selectable skins in regards to $wgSkipSkins.
67
	 * Useful for Special:Preferences and other places where you
68
	 * only want to show skins users _can_ use.
69
	 * @return string[]
70
	 * @since 1.23
71
	 */
72
	public static function getAllowedSkins() {
73
		global $wgSkipSkins;
74
75
		$allowedSkins = self::getSkinNames();
76
77
		foreach ( $wgSkipSkins as $skip ) {
78
			unset( $allowedSkins[$skip] );
79
		}
80
81
		return $allowedSkins;
82
	}
83
84
	/**
85
	 * Normalize a skin preference value to a form that can be loaded.
86
	 *
87
	 * If a skin can't be found, it will fall back to the configured default ($wgDefaultSkin), or the
88
	 * hardcoded default ($wgFallbackSkin) if the default skin is unavailable too.
89
	 *
90
	 * @param string $key 'monobook', 'vector', etc.
91
	 * @return string
92
	 */
93
	static function normalizeKey( $key ) {
94
		global $wgDefaultSkin, $wgFallbackSkin;
95
96
		$skinNames = Skin::getSkinNames();
97
98
		// Make keys lowercase for case-insensitive matching.
99
		$skinNames = array_change_key_case( $skinNames, CASE_LOWER );
100
		$key = strtolower( $key );
101
		$defaultSkin = strtolower( $wgDefaultSkin );
102
		$fallbackSkin = strtolower( $wgFallbackSkin );
103
104
		if ( $key == '' || $key == 'default' ) {
105
			// Don't return the default immediately;
106
			// in a misconfiguration we need to fall back.
107
			$key = $defaultSkin;
108
		}
109
110
		if ( isset( $skinNames[$key] ) ) {
111
			return $key;
112
		}
113
114
		// Older versions of the software used a numeric setting
115
		// in the user preferences.
116
		$fallback = [
117
			0 => $defaultSkin,
118
			2 => 'cologneblue'
119
		];
120
121
		if ( isset( $fallback[$key] ) ) {
122
			$key = $fallback[$key];
123
		}
124
125
		if ( isset( $skinNames[$key] ) ) {
126
			return $key;
127
		} elseif ( isset( $skinNames[$defaultSkin] ) ) {
128
			return $defaultSkin;
129
		} else {
130
			return $fallbackSkin;
131
		}
132
	}
133
134
	/**
135
	 * @return string Skin name
136
	 */
137
	public function getSkinName() {
138
		return $this->skinname;
139
	}
140
141
	/**
142
	 * @param OutputPage $out
143
	 */
144
	function initPage( OutputPage $out ) {
145
146
		$this->preloadExistence();
147
148
	}
149
150
	/**
151
	 * Defines the ResourceLoader modules that should be added to the skin
152
	 * It is recommended that skins wishing to override call parent::getDefaultModules()
153
	 * and substitute out any modules they wish to change by using a key to look them up
154
	 * @return array Array of modules with helper keys for easy overriding
155
	 */
156
	public function getDefaultModules() {
157
		global $wgUseAjax, $wgEnableAPI, $wgEnableWriteAPI;
158
159
		$out = $this->getOutput();
160
		$user = $out->getUser();
161
		$modules = [
162
			// modules that enhance the page content in some way
163
			'content' => [
164
				'mediawiki.page.ready',
165
			],
166
			// modules that exist for legacy reasons
167
			'legacy' => ResourceLoaderStartUpModule::getLegacyModules(),
168
			// modules relating to search functionality
169
			'search' => [],
170
			// modules relating to functionality relating to watching an article
171
			'watch' => [],
172
			// modules which relate to the current users preferences
173
			'user' => [],
174
		];
175
176
		// Add various resources if required
177
		if ( $wgUseAjax && $wgEnableAPI ) {
178
			if ( $wgEnableWriteAPI && $user->isLoggedIn()
179
				&& $user->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
180
				&& $this->getRelevantTitle()->canExist()
181
			) {
182
				$modules['watch'][] = 'mediawiki.page.watch.ajax';
183
			}
184
185
			$modules['search'][] = 'mediawiki.searchSuggest';
186
		}
187
188
		if ( $user->getBoolOption( 'editsectiononrightclick' ) ) {
189
			$modules['user'][] = 'mediawiki.action.view.rightClickEdit';
190
		}
191
192
		// Crazy edit-on-double-click stuff
193
		if ( $out->isArticle() && $user->getOption( 'editondblclick' ) ) {
194
			$modules['user'][] = 'mediawiki.action.view.dblClickEdit';
195
		}
196
		return $modules;
197
	}
198
199
	/**
200
	 * Preload the existence of three commonly-requested pages in a single query
201
	 */
202
	function preloadExistence() {
203
		$titles = [];
204
205
		$user = $this->getUser();
206
		$title = $this->getRelevantTitle();
207
208
		// User/talk link
209
		if ( $user->isLoggedIn() ) {
210
			$titles[] = $user->getUserPage();
211
			$titles[] = $user->getTalkPage();
212
		}
213
214
		// Check, if the page can hold some kind of content, otherwise do nothing
215
		if ( !$title->canExist() ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
216
			// nothing
217
		} elseif ( $title->isTalkPage() ) {
218
			$titles[] = $title->getSubjectPage();
219
		} else {
220
			$titles[] = $title->getTalkPage();
221
		}
222
223
		Hooks::run( 'SkinPreloadExistence', [ &$titles, $this ] );
224
225
		if ( count( $titles ) ) {
226
			$lb = new LinkBatch( $titles );
227
			$lb->setCaller( __METHOD__ );
228
			$lb->execute();
229
		}
230
	}
231
232
	/**
233
	 * Get the current revision ID
234
	 *
235
	 * @return int
236
	 */
237
	public function getRevisionId() {
238
		return $this->getOutput()->getRevisionId();
239
	}
240
241
	/**
242
	 * Whether the revision displayed is the latest revision of the page
243
	 *
244
	 * @return bool
245
	 */
246
	public function isRevisionCurrent() {
247
		$revID = $this->getRevisionId();
248
		return $revID == 0 || $revID == $this->getTitle()->getLatestRevID();
249
	}
250
251
	/**
252
	 * Set the "relevant" title
253
	 * @see self::getRelevantTitle()
254
	 * @param Title $t
255
	 */
256
	public function setRelevantTitle( $t ) {
257
		$this->mRelevantTitle = $t;
258
	}
259
260
	/**
261
	 * Return the "relevant" title.
262
	 * A "relevant" title is not necessarily the actual title of the page.
263
	 * Special pages like Special:MovePage use set the page they are acting on
264
	 * as their "relevant" title, this allows the skin system to display things
265
	 * such as content tabs which belong to to that page instead of displaying
266
	 * a basic special page tab which has almost no meaning.
267
	 *
268
	 * @return Title
269
	 */
270
	public function getRelevantTitle() {
271
		if ( isset( $this->mRelevantTitle ) ) {
272
			return $this->mRelevantTitle;
273
		}
274
		return $this->getTitle();
275
	}
276
277
	/**
278
	 * Set the "relevant" user
279
	 * @see self::getRelevantUser()
280
	 * @param User $u
281
	 */
282
	public function setRelevantUser( $u ) {
283
		$this->mRelevantUser = $u;
284
	}
285
286
	/**
287
	 * Return the "relevant" user.
288
	 * A "relevant" user is similar to a relevant title. Special pages like
289
	 * Special:Contributions mark the user which they are relevant to so that
290
	 * things like the toolbox can display the information they usually are only
291
	 * able to display on a user's userpage and talkpage.
292
	 * @return User
293
	 */
294
	public function getRelevantUser() {
295
		if ( isset( $this->mRelevantUser ) ) {
296
			return $this->mRelevantUser;
297
		}
298
		$title = $this->getRelevantTitle();
299
		if ( $title->hasSubjectNamespace( NS_USER ) ) {
300
			$rootUser = $title->getRootText();
301
			if ( User::isIP( $rootUser ) ) {
302
				$this->mRelevantUser = User::newFromName( $rootUser, false );
303
			} else {
304
				$user = User::newFromName( $rootUser, false );
305
306
				if ( $user ) {
307
					$user->load( User::READ_NORMAL );
308
309
					if ( $user->isLoggedIn() ) {
310
						$this->mRelevantUser = $user;
311
					}
312
				}
313
			}
314
			return $this->mRelevantUser;
315
		}
316
		return null;
317
	}
318
319
	/**
320
	 * Outputs the HTML generated by other functions.
321
	 * @param OutputPage $out
322
	 */
323
	abstract function outputPage( OutputPage $out = null );
324
325
	/**
326
	 * @param array $data
327
	 * @return string
328
	 */
329
	static function makeVariablesScript( $data ) {
330
		if ( $data ) {
331
			return ResourceLoader::makeInlineScript(
332
				ResourceLoader::makeConfigSetScript( $data )
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeConfigSetScript($data) targeting ResourceLoader::makeConfigSetScript() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
333
			);
334
		} else {
335
			return '';
336
		}
337
	}
338
339
	/**
340
	 * Get the query to generate a dynamic stylesheet
341
	 *
342
	 * @return array
343
	 */
344
	public static function getDynamicStylesheetQuery() {
345
		global $wgSquidMaxage;
346
347
		return [
348
				'action' => 'raw',
349
				'maxage' => $wgSquidMaxage,
350
				'usemsgcache' => 'yes',
351
				'ctype' => 'text/css',
352
				'smaxage' => $wgSquidMaxage,
353
			];
354
	}
355
356
	/**
357
	 * Add skin specific stylesheets
358
	 * Calling this method with an $out of anything but the same OutputPage
359
	 * inside ->getOutput() is deprecated. The $out arg is kept
360
	 * for compatibility purposes with skins.
361
	 * @param OutputPage $out
362
	 * @todo delete
363
	 */
364
	abstract function setupSkinUserCss( OutputPage $out );
365
366
	/**
367
	 * TODO: document
368
	 * @param Title $title
369
	 * @return string
370
	 */
371
	function getPageClasses( $title ) {
372
		$numeric = 'ns-' . $title->getNamespace();
373
374
		if ( $title->isSpecialPage() ) {
375
			$type = 'ns-special';
376
			// bug 23315: provide a class based on the canonical special page name without subpages
377
			list( $canonicalName ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
378
			if ( $canonicalName ) {
379
				$type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
380
			} else {
381
				$type .= ' mw-invalidspecialpage';
382
			}
383
		} elseif ( $title->isTalkPage() ) {
384
			$type = 'ns-talk';
385
		} else {
386
			$type = 'ns-subject';
387
		}
388
389
		$name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() );
390
		$root = Sanitizer::escapeClass( 'rootpage-' . $title->getRootTitle()->getPrefixedText() );
391
392
		return "$numeric $type $name $root";
393
	}
394
395
	/**
396
	 * Return values for <html> element
397
	 * @return array Array of associative name-to-value elements for <html> element
398
	 */
399
	public function getHtmlElementAttributes() {
400
		$lang = $this->getLanguage();
401
		return [
402
			'lang' => $lang->getHtmlCode(),
403
			'dir' => $lang->getDir(),
404
			'class' => 'client-nojs',
405
		];
406
	}
407
408
	/**
409
	 * This will be called by OutputPage::headElement when it is creating the
410
	 * "<body>" tag, skins can override it if they have a need to add in any
411
	 * body attributes or classes of their own.
412
	 * @param OutputPage $out
413
	 * @param array $bodyAttrs
414
	 */
415
	function addToBodyAttributes( $out, &$bodyAttrs ) {
416
		// does nothing by default
417
	}
418
419
	/**
420
	 * URL to the logo
421
	 * @return string
422
	 */
423
	function getLogo() {
424
		global $wgLogo;
425
		return $wgLogo;
426
	}
427
428
	/**
429
	 * @return string HTML
430
	 */
431
	function getCategoryLinks() {
432
		global $wgUseCategoryBrowser;
433
434
		$out = $this->getOutput();
435
		$allCats = $out->getCategoryLinks();
436
437
		if ( !count( $allCats ) ) {
438
			return '';
439
		}
440
441
		$embed = "<li>";
442
		$pop = "</li>";
443
444
		$s = '';
445
		$colon = $this->msg( 'colon-separator' )->escaped();
446
447
		if ( !empty( $allCats['normal'] ) ) {
448
			$t = $embed . implode( "{$pop}{$embed}", $allCats['normal'] ) . $pop;
449
450
			$msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped();
451
			$linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text();
452
			$title = Title::newFromText( $linkPage );
453
			$link = $title ? Linker::link( $title, $msg ) : $msg;
454
			$s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' .
455
				$link . $colon . '<ul>' . $t . '</ul>' . '</div>';
456
		}
457
458
		# Hidden categories
459
		if ( isset( $allCats['hidden'] ) ) {
460
			if ( $this->getUser()->getBoolOption( 'showhiddencats' ) ) {
461
				$class = ' mw-hidden-cats-user-shown';
462
			} elseif ( $this->getTitle()->getNamespace() == NS_CATEGORY ) {
463
				$class = ' mw-hidden-cats-ns-shown';
464
			} else {
465
				$class = ' mw-hidden-cats-hidden';
466
			}
467
468
			$s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" .
469
				$this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
470
				$colon . '<ul>' . $embed . implode( "{$pop}{$embed}", $allCats['hidden'] ) . $pop . '</ul>' .
471
				'</div>';
472
		}
473
474
		# optional 'dmoz-like' category browser. Will be shown under the list
475
		# of categories an article belong to
476
		if ( $wgUseCategoryBrowser ) {
477
			$s .= '<br /><hr />';
478
479
			# get a big array of the parents tree
480
			$parenttree = $this->getTitle()->getParentCategoryTree();
481
			# Skin object passed by reference cause it can not be
482
			# accessed under the method subfunction drawCategoryBrowser
483
			$tempout = explode( "\n", $this->drawCategoryBrowser( $parenttree ) );
484
			# Clean out bogus first entry and sort them
485
			unset( $tempout[0] );
486
			asort( $tempout );
487
			# Output one per line
488
			$s .= implode( "<br />\n", $tempout );
489
		}
490
491
		return $s;
492
	}
493
494
	/**
495
	 * Render the array as a series of links.
496
	 * @param array $tree Categories tree returned by Title::getParentCategoryTree
497
	 * @return string Separated by &gt;, terminate with "\n"
498
	 */
499
	function drawCategoryBrowser( $tree ) {
500
		$return = '';
501
502
		foreach ( $tree as $element => $parent ) {
503
			if ( empty( $parent ) ) {
504
				# element start a new list
505
				$return .= "\n";
506
			} else {
507
				# grab the others elements
508
				$return .= $this->drawCategoryBrowser( $parent ) . ' &gt; ';
509
			}
510
511
			# add our current element to the list
512
			$eltitle = Title::newFromText( $element );
513
			$return .= Linker::link( $eltitle, htmlspecialchars( $eltitle->getText() ) );
0 ignored issues
show
Bug introduced by
It seems like $eltitle defined by \Title::newFromText($element) on line 512 can be null; however, Linker::link() 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...
514
		}
515
516
		return $return;
517
	}
518
519
	/**
520
	 * @return string HTML
521
	 */
522
	function getCategories() {
523
		$out = $this->getOutput();
524
		$catlinks = $this->getCategoryLinks();
525
526
		// Check what we're showing
527
		$allCats = $out->getCategoryLinks();
528
		$showHidden = $this->getUser()->getBoolOption( 'showhiddencats' ) ||
529
						$this->getTitle()->getNamespace() == NS_CATEGORY;
530
531
		$classes = [ 'catlinks' ];
532
		if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
533
			$classes[] = 'catlinks-allhidden';
534
		}
535
536
		return Html::rawElement(
537
			'div',
538
			[ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
539
			$catlinks
540
		);
541
	}
542
543
	/**
544
	 * This runs a hook to allow extensions placing their stuff after content
545
	 * and article metadata (e.g. categories).
546
	 * Note: This function has nothing to do with afterContent().
547
	 *
548
	 * This hook is placed here in order to allow using the same hook for all
549
	 * skins, both the SkinTemplate based ones and the older ones, which directly
550
	 * use this class to get their data.
551
	 *
552
	 * The output of this function gets processed in SkinTemplate::outputPage() for
553
	 * the SkinTemplate based skins, all other skins should directly echo it.
554
	 *
555
	 * @return string Empty by default, if not changed by any hook function.
556
	 */
557
	protected function afterContentHook() {
558
		$data = '';
559
560
		if ( Hooks::run( 'SkinAfterContent', [ &$data, $this ] ) ) {
561
			// adding just some spaces shouldn't toggle the output
562
			// of the whole <div/>, so we use trim() here
563
			if ( trim( $data ) != '' ) {
564
				// Doing this here instead of in the skins to
565
				// ensure that the div has the same ID in all
566
				// skins
567
				$data = "<div id='mw-data-after-content'>\n" .
568
					"\t$data\n" .
569
					"</div>\n";
570
			}
571
		} else {
572
			wfDebug( "Hook SkinAfterContent changed output processing.\n" );
573
		}
574
575
		return $data;
576
	}
577
578
	/**
579
	 * Generate debug data HTML for displaying at the bottom of the main content
580
	 * area.
581
	 * @return string HTML containing debug data, if enabled (otherwise empty).
582
	 */
583
	protected function generateDebugHTML() {
584
		return MWDebug::getHTMLDebugLog();
585
	}
586
587
	/**
588
	 * This gets called shortly before the "</body>" tag.
589
	 *
590
	 * @return string HTML-wrapped JS code to be put before "</body>"
591
	 */
592
	function bottomScripts() {
593
		// TODO and the suckage continues. This function is really just a wrapper around
594
		// OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
595
		// up at some point
596
		$bottomScriptText = $this->getOutput()->getBottomScripts();
597
		Hooks::run( 'SkinAfterBottomScripts', [ $this, &$bottomScriptText ] );
598
599
		return $bottomScriptText;
600
	}
601
602
	/**
603
	 * Text with the permalink to the source page,
604
	 * usually shown on the footer of a printed page
605
	 *
606
	 * @return string HTML text with an URL
607
	 */
608
	function printSource() {
609
		$oldid = $this->getRevisionId();
610
		if ( $oldid ) {
611
			$canonicalUrl = $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid );
612
			$url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
613
		} else {
614
			// oldid not available for non existing pages
615
			$url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL() ) );
616
		}
617
618
		return $this->msg( 'retrievedfrom' )
619
			->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
620
			->parse();
621
	}
622
623
	/**
624
	 * @return string HTML
625
	 */
626
	function getUndeleteLink() {
627
		$action = $this->getRequest()->getVal( 'action', 'view' );
628
629
		if ( $this->getTitle()->userCan( 'deletedhistory', $this->getUser() ) &&
630
			( !$this->getTitle()->exists() || $action == 'history' ) ) {
631
			$n = $this->getTitle()->isDeleted();
632
633
			if ( $n ) {
634
				if ( $this->getTitle()->quickUserCan( 'undelete', $this->getUser() ) ) {
635
					$msg = 'thisisdeleted';
636
				} else {
637
					$msg = 'viewdeleted';
638
				}
639
640
				return $this->msg( $msg )->rawParams(
641
					Linker::linkKnown(
642
						SpecialPage::getTitleFor( 'Undelete', $this->getTitle()->getPrefixedDBkey() ),
643
						$this->msg( 'restorelink' )->numParams( $n )->escaped() )
644
					)->escaped();
645
			}
646
		}
647
648
		return '';
649
	}
650
651
	/**
652
	 * @return string
653
	 */
654
	function subPageSubtitle() {
655
		$out = $this->getOutput();
656
		$subpages = '';
657
658
		if ( !Hooks::run( 'SkinSubPageSubtitle', [ &$subpages, $this, $out ] ) ) {
659
			return $subpages;
660
		}
661
662
		if ( $out->isArticle() && MWNamespace::hasSubpages( $out->getTitle()->getNamespace() ) ) {
663
			$ptext = $this->getTitle()->getPrefixedText();
664
			if ( strpos( $ptext, '/' ) !== false ) {
665
				$links = explode( '/', $ptext );
666
				array_pop( $links );
667
				$c = 0;
668
				$growinglink = '';
669
				$display = '';
670
				$lang = $this->getLanguage();
671
672
				foreach ( $links as $link ) {
673
					$growinglink .= $link;
674
					$display .= $link;
675
					$linkObj = Title::newFromText( $growinglink );
676
677
					if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
678
						$getlink = Linker::linkKnown(
679
							$linkObj,
680
							htmlspecialchars( $display )
681
						);
682
683
						$c++;
684
685
						if ( $c > 1 ) {
686
							$subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
687
						} else {
688
							$subpages .= '&lt; ';
689
						}
690
691
						$subpages .= $getlink;
692
						$display = '';
693
					} else {
694
						$display .= '/';
695
					}
696
					$growinglink .= '/';
697
				}
698
			}
699
		}
700
701
		return $subpages;
702
	}
703
704
	/**
705
	 * @deprecated since 1.27, feature removed
706
	 * @return bool Always false
707
	 */
708
	function showIPinHeader() {
709
		wfDeprecated( __METHOD__, '1.27' );
710
		return false;
711
	}
712
713
	/**
714
	 * @return string
715
	 */
716
	function getSearchLink() {
717
		$searchPage = SpecialPage::getTitleFor( 'Search' );
718
		return $searchPage->getLocalURL();
719
	}
720
721
	/**
722
	 * @return string
723
	 */
724
	function escapeSearchLink() {
725
		return htmlspecialchars( $this->getSearchLink() );
726
	}
727
728
	/**
729
	 * @param string $type
730
	 * @return string
731
	 */
732
	function getCopyright( $type = 'detect' ) {
733
		global $wgRightsPage, $wgRightsUrl, $wgRightsText;
734
735
		if ( $type == 'detect' ) {
736
			if ( !$this->isRevisionCurrent()
737
				&& !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
738
			) {
739
				$type = 'history';
740
			} else {
741
				$type = 'normal';
742
			}
743
		}
744
745
		if ( $type == 'history' ) {
746
			$msg = 'history_copyright';
747
		} else {
748
			$msg = 'copyright';
749
		}
750
751
		if ( $wgRightsPage ) {
752
			$title = Title::newFromText( $wgRightsPage );
753
			$link = Linker::linkKnown( $title, $wgRightsText );
754
		} elseif ( $wgRightsUrl ) {
755
			$link = Linker::makeExternalLink( $wgRightsUrl, $wgRightsText );
756
		} elseif ( $wgRightsText ) {
757
			$link = $wgRightsText;
758
		} else {
759
			# Give up now
760
			return '';
761
		}
762
763
		// Allow for site and per-namespace customization of copyright notice.
764
		// @todo Remove deprecated $forContent param from hook handlers and then remove here.
765
		$forContent = true;
766
767
		Hooks::run(
768
			'SkinCopyrightFooter',
769
			[ $this->getTitle(), $type, &$msg, &$link, &$forContent ]
770
		);
771
772
		return $this->msg( $msg )->rawParams( $link )->text();
773
	}
774
775
	/**
776
	 * @return null|string
777
	 */
778
	function getCopyrightIcon() {
779
		global $wgRightsUrl, $wgRightsText, $wgRightsIcon, $wgFooterIcons;
780
781
		$out = '';
782
783
		if ( $wgFooterIcons['copyright']['copyright'] ) {
784
			$out = $wgFooterIcons['copyright']['copyright'];
785
		} elseif ( $wgRightsIcon ) {
786
			$icon = htmlspecialchars( $wgRightsIcon );
787
788
			if ( $wgRightsUrl ) {
789
				$url = htmlspecialchars( $wgRightsUrl );
790
				$out .= '<a href="' . $url . '">';
791
			}
792
793
			$text = htmlspecialchars( $wgRightsText );
794
			$out .= "<img src=\"$icon\" alt=\"$text\" width=\"88\" height=\"31\" />";
795
796
			if ( $wgRightsUrl ) {
797
				$out .= '</a>';
798
			}
799
		}
800
801
		return $out;
802
	}
803
804
	/**
805
	 * Gets the powered by MediaWiki icon.
806
	 * @return string
807
	 */
808
	function getPoweredBy() {
809
		global $wgResourceBasePath;
810
811
		$url1 = htmlspecialchars(
812
			"$wgResourceBasePath/resources/assets/poweredby_mediawiki_88x31.png"
813
		);
814
		$url1_5 = htmlspecialchars(
815
			"$wgResourceBasePath/resources/assets/poweredby_mediawiki_132x47.png"
816
		);
817
		$url2 = htmlspecialchars(
818
			"$wgResourceBasePath/resources/assets/poweredby_mediawiki_176x62.png"
819
		);
820
		$text = '<a href="//www.mediawiki.org/"><img src="' . $url1
821
			. '" srcset="' . $url1_5 . ' 1.5x, ' . $url2 . ' 2x" '
822
			. 'height="31" width="88" alt="Powered by MediaWiki" /></a>';
823
		Hooks::run( 'SkinGetPoweredBy', [ &$text, $this ] );
824
		return $text;
825
	}
826
827
	/**
828
	 * Get the timestamp of the latest revision, formatted in user language
829
	 *
830
	 * @return string
831
	 */
832
	protected function lastModified() {
833
		$timestamp = $this->getOutput()->getRevisionTimestamp();
834
835
		# No cached timestamp, load it from the database
836
		if ( $timestamp === null ) {
837
			$timestamp = Revision::getTimestampFromId( $this->getTitle(), $this->getRevisionId() );
0 ignored issues
show
Bug introduced by
It seems like $this->getTitle() can be null; however, getTimestampFromId() 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...
838
		}
839
840
		if ( $timestamp ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $timestamp of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
841
			$d = $this->getLanguage()->userDate( $timestamp, $this->getUser() );
842
			$t = $this->getLanguage()->userTime( $timestamp, $this->getUser() );
843
			$s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->parse();
844
		} else {
845
			$s = '';
846
		}
847
848
		if ( wfGetLB()->getLaggedSlaveMode() ) {
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
849
			$s .= ' <strong>' . $this->msg( 'laggedslavemode' )->parse() . '</strong>';
850
		}
851
852
		return $s;
853
	}
854
855
	/**
856
	 * @param string $align
857
	 * @return string
858
	 */
859
	function logoText( $align = '' ) {
860
		if ( $align != '' ) {
861
			$a = " style='float: {$align};'";
862
		} else {
863
			$a = '';
864
		}
865
866
		$mp = $this->msg( 'mainpage' )->escaped();
867
		$mptitle = Title::newMainPage();
868
		$url = ( is_object( $mptitle ) ? htmlspecialchars( $mptitle->getLocalURL() ) : '' );
869
870
		$logourl = $this->getLogo();
871
		$s = "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
872
873
		return $s;
874
	}
875
876
	/**
877
	 * Renders a $wgFooterIcons icon according to the method's arguments
878
	 * @param array $icon The icon to build the html for, see $wgFooterIcons
879
	 *   for the format of this array.
880
	 * @param bool|string $withImage Whether to use the icon's image or output
881
	 *   a text-only footericon.
882
	 * @return string HTML
883
	 */
884
	function makeFooterIcon( $icon, $withImage = 'withImage' ) {
885
		if ( is_string( $icon ) ) {
886
			$html = $icon;
887
		} else { // Assuming array
888
			$url = isset( $icon["url"] ) ? $icon["url"] : null;
889
			unset( $icon["url"] );
890
			if ( isset( $icon["src"] ) && $withImage === 'withImage' ) {
891
				// do this the lazy way, just pass icon data as an attribute array
892
				$html = Html::element( 'img', $icon );
893
			} else {
894
				$html = htmlspecialchars( $icon["alt"] );
895
			}
896
			if ( $url ) {
897
				$html = Html::rawElement( 'a', [ "href" => $url ], $html );
898
			}
899
		}
900
		return $html;
901
	}
902
903
	/**
904
	 * Gets the link to the wiki's main page.
905
	 * @return string
906
	 */
907
	function mainPageLink() {
908
		$s = Linker::linkKnown(
909
			Title::newMainPage(),
910
			$this->msg( 'mainpage' )->escaped()
911
		);
912
913
		return $s;
914
	}
915
916
	/**
917
	 * Returns an HTML link for use in the footer
918
	 * @param string $desc The i18n message key for the link text
919
	 * @param string $page The i18n message key for the page to link to
920
	 * @return string HTML anchor
921
	 */
922
	public function footerLink( $desc, $page ) {
923
		// if the link description has been set to "-" in the default language,
924
		if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
925
			// then it is disabled, for all languages.
926
			return '';
927
		} else {
928
			// Otherwise, we display the link for the user, described in their
929
			// language (which may or may not be the same as the default language),
930
			// but we make the link target be the one site-wide page.
931
			$title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
932
933
			if ( !$title ) {
934
				return '';
935
			}
936
937
			return Linker::linkKnown(
938
				$title,
939
				$this->msg( $desc )->escaped()
940
			);
941
		}
942
	}
943
944
	/**
945
	 * Gets the link to the wiki's privacy policy page.
946
	 * @return string HTML
947
	 */
948
	function privacyLink() {
949
		return $this->footerLink( 'privacy', 'privacypage' );
950
	}
951
952
	/**
953
	 * Gets the link to the wiki's about page.
954
	 * @return string HTML
955
	 */
956
	function aboutLink() {
957
		return $this->footerLink( 'aboutsite', 'aboutpage' );
958
	}
959
960
	/**
961
	 * Gets the link to the wiki's general disclaimers page.
962
	 * @return string HTML
963
	 */
964
	function disclaimerLink() {
965
		return $this->footerLink( 'disclaimers', 'disclaimerpage' );
966
	}
967
968
	/**
969
	 * Return URL options for the 'edit page' link.
970
	 * This may include an 'oldid' specifier, if the current page view is such.
971
	 *
972
	 * @return array
973
	 * @private
974
	 */
975
	function editUrlOptions() {
976
		$options = [ 'action' => 'edit' ];
977
978
		if ( !$this->isRevisionCurrent() ) {
979
			$options['oldid'] = intval( $this->getRevisionId() );
980
		}
981
982
		return $options;
983
	}
984
985
	/**
986
	 * @param User|int $id
987
	 * @return bool
988
	 */
989
	function showEmailUser( $id ) {
990
		if ( $id instanceof User ) {
991
			$targetUser = $id;
992
		} else {
993
			$targetUser = User::newFromId( $id );
994
		}
995
996
		# The sending user must have a confirmed email address and the target
997
		# user must have a confirmed email address and allow emails from users.
998
		return $this->getUser()->canSendEmail() &&
999
			$targetUser->canReceiveEmail();
1000
	}
1001
1002
	/**
1003
	 * Return a fully resolved style path url to images or styles stored in the current skins's folder.
1004
	 * This method returns a url resolved using the configured skin style path
1005
	 * and includes the style version inside of the url.
1006
	 *
1007
	 * Requires $stylename to be set, otherwise throws MWException.
1008
	 *
1009
	 * @param string $name The name or path of a skin resource file
1010
	 * @return string The fully resolved style path url including styleversion
1011
	 * @throws MWException
1012
	 */
1013
	function getSkinStylePath( $name ) {
1014
		global $wgStylePath, $wgStyleVersion;
1015
1016
		if ( $this->stylename === null ) {
1017
			$class = get_class( $this );
1018
			throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
1019
		}
1020
1021
		return "$wgStylePath/{$this->stylename}/$name?$wgStyleVersion";
1022
	}
1023
1024
	/* these are used extensively in SkinTemplate, but also some other places */
1025
1026
	/**
1027
	 * @param string $urlaction
1028
	 * @return string
1029
	 */
1030
	static function makeMainPageUrl( $urlaction = '' ) {
1031
		$title = Title::newMainPage();
1032
		self::checkTitle( $title, '' );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newMainPage() on line 1031 can be null; however, Skin::checkTitle() 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...
1033
1034
		return $title->getLocalURL( $urlaction );
1035
	}
1036
1037
	/**
1038
	 * Make a URL for a Special Page using the given query and protocol.
1039
	 *
1040
	 * If $proto is set to null, make a local URL. Otherwise, make a full
1041
	 * URL with the protocol specified.
1042
	 *
1043
	 * @param string $name Name of the Special page
1044
	 * @param string $urlaction Query to append
1045
	 * @param string|null $proto Protocol to use or null for a local URL
1046
	 * @return string
1047
	 */
1048
	static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1049
		$title = SpecialPage::getSafeTitleFor( $name );
1050
		if ( is_null( $proto ) ) {
1051
			return $title->getLocalURL( $urlaction );
1052
		} else {
1053
			return $title->getFullURL( $urlaction, false, $proto );
1054
		}
1055
	}
1056
1057
	/**
1058
	 * @param string $name
1059
	 * @param string $subpage
1060
	 * @param string $urlaction
1061
	 * @return string
1062
	 */
1063
	static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1064
		$title = SpecialPage::getSafeTitleFor( $name, $subpage );
1065
		return $title->getLocalURL( $urlaction );
1066
	}
1067
1068
	/**
1069
	 * @param string $name
1070
	 * @param string $urlaction
1071
	 * @return string
1072
	 */
1073
	static function makeI18nUrl( $name, $urlaction = '' ) {
1074
		$title = Title::newFromText( wfMessage( $name )->inContentLanguage()->text() );
1075
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText(wfMe...tentLanguage()->text()) on line 1074 can be null; however, Skin::checkTitle() 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...
1076
		return $title->getLocalURL( $urlaction );
1077
	}
1078
1079
	/**
1080
	 * @param string $name
1081
	 * @param string $urlaction
1082
	 * @return string
1083
	 */
1084
	static function makeUrl( $name, $urlaction = '' ) {
1085
		$title = Title::newFromText( $name );
1086
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($name) on line 1085 can be null; however, Skin::checkTitle() 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...
1087
1088
		return $title->getLocalURL( $urlaction );
1089
	}
1090
1091
	/**
1092
	 * If url string starts with http, consider as external URL, else
1093
	 * internal
1094
	 * @param string $name
1095
	 * @return string URL
1096
	 */
1097
	static function makeInternalOrExternalUrl( $name ) {
1098
		if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
1099
			return $name;
1100
		} else {
1101
			return self::makeUrl( $name );
1102
		}
1103
	}
1104
1105
	/**
1106
	 * this can be passed the NS number as defined in Language.php
1107
	 * @param string $name
1108
	 * @param string $urlaction
1109
	 * @param int $namespace
1110
	 * @return string
1111
	 */
1112
	static function makeNSUrl( $name, $urlaction = '', $namespace = NS_MAIN ) {
1113
		$title = Title::makeTitleSafe( $namespace, $name );
1114
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $name) on line 1113 can be null; however, Skin::checkTitle() 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...
1115
1116
		return $title->getLocalURL( $urlaction );
1117
	}
1118
1119
	/**
1120
	 * these return an array with the 'href' and boolean 'exists'
1121
	 * @param string $name
1122
	 * @param string $urlaction
1123
	 * @return array
1124
	 */
1125 View Code Duplication
	static function makeUrlDetails( $name, $urlaction = '' ) {
1126
		$title = Title::newFromText( $name );
1127
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($name) on line 1126 can be null; however, Skin::checkTitle() 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...
1128
1129
		return [
1130
			'href' => $title->getLocalURL( $urlaction ),
1131
			'exists' => $title->isKnown(),
1132
		];
1133
	}
1134
1135
	/**
1136
	 * Make URL details where the article exists (or at least it's convenient to think so)
1137
	 * @param string $name Article name
1138
	 * @param string $urlaction
1139
	 * @return array
1140
	 */
1141 View Code Duplication
	static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1142
		$title = Title::newFromText( $name );
1143
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($name) on line 1142 can be null; however, Skin::checkTitle() 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...
1144
1145
		return [
1146
			'href' => $title->getLocalURL( $urlaction ),
1147
			'exists' => true
1148
		];
1149
	}
1150
1151
	/**
1152
	 * make sure we have some title to operate on
1153
	 *
1154
	 * @param Title $title
1155
	 * @param string $name
1156
	 */
1157
	static function checkTitle( &$title, $name ) {
1158
		if ( !is_object( $title ) ) {
1159
			$title = Title::newFromText( $name );
1160
			if ( !is_object( $title ) ) {
1161
				$title = Title::newFromText( '--error: link target missing--' );
1162
			}
1163
		}
1164
	}
1165
1166
	/**
1167
	 * Build an array that represents the sidebar(s), the navigation bar among them.
1168
	 *
1169
	 * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
1170
	 *
1171
	 * The format of the returned array is array( heading => content, ... ), where:
1172
	 * - heading is the heading of a navigation portlet. It is either:
1173
	 *   - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
1174
	 *   - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
1175
	 *   - plain text, which should be HTML-escaped by the skin
1176
	 * - content is the contents of the portlet. It is either:
1177
	 *   - HTML text (<ul><li>...</li>...</ul>)
1178
	 *   - array of link data in a format accepted by BaseTemplate::makeListItem()
1179
	 *   - (for a magic string as a key, any value)
1180
	 *
1181
	 * Note that extensions can control the sidebar contents using the SkinBuildSidebar hook
1182
	 * and can technically insert anything in here; skin creators are expected to handle
1183
	 * values described above.
1184
	 *
1185
	 * @return array
1186
	 */
1187
	function buildSidebar() {
1188
		global $wgEnableSidebarCache, $wgSidebarCacheExpiry;
1189
1190
		$that = $this;
1191
		$callback = function () use ( $that ) {
1192
			$bar = [];
1193
			$that->addToSidebar( $bar, 'sidebar' );
1194
			Hooks::run( 'SkinBuildSidebar', [ $that, &$bar ] );
1195
1196
			return $bar;
1197
		};
1198
1199
		if ( $wgEnableSidebarCache ) {
1200
			$cache = ObjectCache::getMainWANInstance();
1201
			$sidebar = $cache->getWithSetCallback(
1202
				$cache->makeKey( 'sidebar', $this->getLanguage()->getCode() ),
1203
				MessageCache::singleton()->isDisabled()
1204
					? $cache::TTL_UNCACHEABLE // bug T133069
1205
					: $wgSidebarCacheExpiry,
1206
				$callback,
1207
				[ 'lockTSE' => 30 ]
1208
			);
1209
		} else {
1210
			$sidebar = $callback();
1211
		}
1212
1213
		// Apply post-processing to the cached value
1214
		Hooks::run( 'SidebarBeforeOutput', [ $this, &$sidebar ] );
1215
1216
		return $sidebar;
1217
	}
1218
1219
	/**
1220
	 * Add content from a sidebar system message
1221
	 * Currently only used for MediaWiki:Sidebar (but may be used by Extensions)
1222
	 *
1223
	 * This is just a wrapper around addToSidebarPlain() for backwards compatibility
1224
	 *
1225
	 * @param array $bar
1226
	 * @param string $message
1227
	 */
1228
	public function addToSidebar( &$bar, $message ) {
1229
		$this->addToSidebarPlain( $bar, wfMessage( $message )->inContentLanguage()->plain() );
1230
	}
1231
1232
	/**
1233
	 * Add content from plain text
1234
	 * @since 1.17
1235
	 * @param array $bar
1236
	 * @param string $text
1237
	 * @return array
1238
	 */
1239
	function addToSidebarPlain( &$bar, $text ) {
1240
		$lines = explode( "\n", $text );
1241
1242
		$heading = '';
1243
1244
		foreach ( $lines as $line ) {
1245
			if ( strpos( $line, '*' ) !== 0 ) {
1246
				continue;
1247
			}
1248
			$line = rtrim( $line, "\r" ); // for Windows compat
1249
1250
			if ( strpos( $line, '**' ) !== 0 ) {
1251
				$heading = trim( $line, '* ' );
1252
				if ( !array_key_exists( $heading, $bar ) ) {
1253
					$bar[$heading] = [];
1254
				}
1255
			} else {
1256
				$line = trim( $line, '* ' );
1257
1258
				if ( strpos( $line, '|' ) !== false ) { // sanity check
1259
					$line = MessageCache::singleton()->transform( $line, false, null, $this->getTitle() );
1260
					$line = array_map( 'trim', explode( '|', $line, 2 ) );
1261
					if ( count( $line ) !== 2 ) {
1262
						// Second sanity check, could be hit by people doing
1263
						// funky stuff with parserfuncs... (bug 33321)
1264
						continue;
1265
					}
1266
1267
					$extraAttribs = [];
1268
1269
					$msgLink = $this->msg( $line[0] )->inContentLanguage();
1270
					if ( $msgLink->exists() ) {
1271
						$link = $msgLink->text();
1272
						if ( $link == '-' ) {
1273
							continue;
1274
						}
1275
					} else {
1276
						$link = $line[0];
1277
					}
1278
					$msgText = $this->msg( $line[1] );
1279
					if ( $msgText->exists() ) {
1280
						$text = $msgText->text();
1281
					} else {
1282
						$text = $line[1];
1283
					}
1284
1285
					if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
1286
						$href = $link;
1287
1288
						// Parser::getExternalLinkAttribs won't work here because of the Namespace things
1289
						global $wgNoFollowLinks, $wgNoFollowDomainExceptions;
1290
						if ( $wgNoFollowLinks && !wfMatchesDomainList( $href, $wgNoFollowDomainExceptions ) ) {
1291
							$extraAttribs['rel'] = 'nofollow';
1292
						}
1293
1294
						global $wgExternalLinkTarget;
1295
						if ( $wgExternalLinkTarget ) {
1296
							$extraAttribs['target'] = $wgExternalLinkTarget;
1297
						}
1298
					} else {
1299
						$title = Title::newFromText( $link );
1300
1301
						if ( $title ) {
1302
							$title = $title->fixSpecialName();
1303
							$href = $title->getLinkURL();
1304
						} else {
1305
							$href = 'INVALID-TITLE';
1306
						}
1307
					}
1308
1309
					$bar[$heading][] = array_merge( [
1310
						'text' => $text,
1311
						'href' => $href,
1312
						'id' => 'n-' . Sanitizer::escapeId( strtr( $line[1], ' ', '-' ), 'noninitial' ),
1313
						'active' => false
1314
					], $extraAttribs );
1315
				} else {
1316
					continue;
1317
				}
1318
			}
1319
		}
1320
1321
		return $bar;
1322
	}
1323
1324
	/**
1325
	 * Gets new talk page messages for the current user and returns an
1326
	 * appropriate alert message (or an empty string if there are no messages)
1327
	 * @return string
1328
	 */
1329
	function getNewtalks() {
1330
1331
		$newMessagesAlert = '';
1332
		$user = $this->getUser();
1333
		$newtalks = $user->getNewMessageLinks();
1334
		$out = $this->getOutput();
1335
1336
		// Allow extensions to disable or modify the new messages alert
1337
		if ( !Hooks::run( 'GetNewMessagesAlert', [ &$newMessagesAlert, $newtalks, $user, $out ] ) ) {
1338
			return '';
1339
		}
1340
		if ( $newMessagesAlert ) {
1341
			return $newMessagesAlert;
1342
		}
1343
1344
		if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
1345
			$uTalkTitle = $user->getTalkPage();
1346
			$lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null;
1347
			$nofAuthors = 0;
1348
			if ( $lastSeenRev !== null ) {
1349
				$plural = true; // Default if we have a last seen revision: if unknown, use plural
1350
				$latestRev = Revision::newFromTitle( $uTalkTitle, false, Revision::READ_NORMAL );
1351
				if ( $latestRev !== null ) {
1352
					// Singular if only 1 unseen revision, plural if several unseen revisions.
1353
					$plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1354
					$nofAuthors = $uTalkTitle->countAuthorsBetween(
1355
						$lastSeenRev, $latestRev, 10, 'include_new' );
1356
				}
1357
			} else {
1358
				// Singular if no revision -> diff link will show latest change only in any case
1359
				$plural = false;
1360
			}
1361
			$plural = $plural ? 999 : 1;
1362
			// 999 signifies "more than one revision". We don't know how many, and even if we did,
1363
			// the number of revisions or authors is not necessarily the same as the number of
1364
			// "messages".
1365
			$newMessagesLink = Linker::linkKnown(
1366
				$uTalkTitle,
1367
				$this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(),
1368
				[],
1369
				[ 'redirect' => 'no' ]
1370
			);
1371
1372
			$newMessagesDiffLink = Linker::linkKnown(
1373
				$uTalkTitle,
1374
				$this->msg( 'newmessagesdifflinkplural' )->params( $plural )->escaped(),
1375
				[],
1376
				$lastSeenRev !== null
1377
					? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1378
					: [ 'diff' => 'cur' ]
1379
			);
1380
1381
			if ( $nofAuthors >= 1 && $nofAuthors <= 10 ) {
1382
				$newMessagesAlert = $this->msg(
1383
					'youhavenewmessagesfromusers',
1384
					$newMessagesLink,
1385
					$newMessagesDiffLink
1386
				)->numParams( $nofAuthors, $plural );
1387
			} else {
1388
				// $nofAuthors === 11 signifies "11 or more" ("more than 10")
1389
				$newMessagesAlert = $this->msg(
1390
					$nofAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
1391
					$newMessagesLink,
1392
					$newMessagesDiffLink
1393
				)->numParams( $plural );
1394
			}
1395
			$newMessagesAlert = $newMessagesAlert->text();
1396
			# Disable CDN cache
1397
			$out->setCdnMaxage( 0 );
1398
		} elseif ( count( $newtalks ) ) {
1399
			$sep = $this->msg( 'newtalkseparator' )->escaped();
1400
			$msgs = [];
1401
1402
			foreach ( $newtalks as $newtalk ) {
1403
				$msgs[] = Xml::element(
1404
					'a',
1405
					[ 'href' => $newtalk['link'] ], $newtalk['wiki']
1406
				);
1407
			}
1408
			$parts = implode( $sep, $msgs );
1409
			$newMessagesAlert = $this->msg( 'youhavenewmessagesmulti' )->rawParams( $parts )->escaped();
1410
			$out->setCdnMaxage( 0 );
1411
		}
1412
1413
		return $newMessagesAlert;
1414
	}
1415
1416
	/**
1417
	 * Get a cached notice
1418
	 *
1419
	 * @param string $name Message name, or 'default' for $wgSiteNotice
1420
	 * @return string|bool HTML fragment, or false to indicate that the caller
1421
	 *   should fall back to the next notice in its sequence
1422
	 */
1423
	private function getCachedNotice( $name ) {
1424
		global $wgRenderHashAppend, $parserMemc, $wgContLang;
1425
1426
		$needParse = false;
1427
1428
		if ( $name === 'default' ) {
1429
			// special case
1430
			global $wgSiteNotice;
1431
			$notice = $wgSiteNotice;
1432
			if ( empty( $notice ) ) {
1433
				return false;
1434
			}
1435
		} else {
1436
			$msg = $this->msg( $name )->inContentLanguage();
1437
			if ( $msg->isBlank() ) {
1438
				return '';
1439
			} elseif ( $msg->isDisabled() ) {
1440
				return false;
1441
			}
1442
			$notice = $msg->plain();
1443
		}
1444
1445
		// Use the extra hash appender to let eg SSL variants separately cache.
1446
		$key = wfMemcKey( $name . $wgRenderHashAppend );
1447
		$cachedNotice = $parserMemc->get( $key );
1448
		if ( is_array( $cachedNotice ) ) {
1449
			if ( md5( $notice ) == $cachedNotice['hash'] ) {
1450
				$notice = $cachedNotice['html'];
1451
			} else {
1452
				$needParse = true;
1453
			}
1454
		} else {
1455
			$needParse = true;
1456
		}
1457
1458
		if ( $needParse ) {
1459
			$parsed = $this->getOutput()->parse( $notice );
1460
			$parserMemc->set( $key, [ 'html' => $parsed, 'hash' => md5( $notice ) ], 600 );
1461
			$notice = $parsed;
1462
		}
1463
1464
		$notice = Html::rawElement( 'div', [ 'id' => 'localNotice',
1465
			'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ], $notice );
1466
		return $notice;
1467
	}
1468
1469
	/**
1470
	 * Get the site notice
1471
	 *
1472
	 * @return string HTML fragment
1473
	 */
1474
	function getSiteNotice() {
1475
		$siteNotice = '';
1476
1477
		if ( Hooks::run( 'SiteNoticeBefore', [ &$siteNotice, $this ] ) ) {
1478
			if ( is_object( $this->getUser() ) && $this->getUser()->isLoggedIn() ) {
1479
				$siteNotice = $this->getCachedNotice( 'sitenotice' );
1480
			} else {
1481
				$anonNotice = $this->getCachedNotice( 'anonnotice' );
1482
				if ( $anonNotice === false ) {
1483
					$siteNotice = $this->getCachedNotice( 'sitenotice' );
1484
				} else {
1485
					$siteNotice = $anonNotice;
1486
				}
1487
			}
1488
			if ( $siteNotice === false ) {
1489
				$siteNotice = $this->getCachedNotice( 'default' );
1490
			}
1491
		}
1492
1493
		Hooks::run( 'SiteNoticeAfter', [ &$siteNotice, $this ] );
1494
		return $siteNotice;
1495
	}
1496
1497
	/**
1498
	 * Create a section edit link.  This supersedes editSectionLink() and
1499
	 * editSectionLinkForOther().
1500
	 *
1501
	 * @param Title $nt The title being linked to (may not be the same as
1502
	 *   the current page, if the section is included from a template)
1503
	 * @param string $section The designation of the section being pointed to,
1504
	 *   to be included in the link, like "&section=$section"
1505
	 * @param string $tooltip The tooltip to use for the link: will be escaped
1506
	 *   and wrapped in the 'editsectionhint' message
1507
	 * @param string $lang Language code
1508
	 * @return string HTML to use for edit link
1509
	 */
1510
	public function doEditSectionLink( Title $nt, $section, $tooltip = null, $lang = false ) {
1511
		// HTML generated here should probably have userlangattributes
1512
		// added to it for LTR text on RTL pages
1513
1514
		$lang = wfGetLangObj( $lang );
1515
1516
		$attribs = [];
1517
		if ( !is_null( $tooltip ) ) {
1518
			# Bug 25462: undo double-escaping.
1519
			$tooltip = Sanitizer::decodeCharReferences( $tooltip );
1520
			$attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
1521
				->inLanguage( $lang )->text();
1522
		}
1523
1524
		$links = [
1525
			'editsection' => [
1526
				'text' => wfMessage( 'editsection' )->inLanguage( $lang )->escaped(),
1527
				'targetTitle' => $nt,
1528
				'attribs' => $attribs,
1529
				'query' => [ 'action' => 'edit', 'section' => $section ],
1530
				'options' => [ 'noclasses', 'known' ]
1531
			]
1532
		];
1533
1534
		Hooks::run( 'SkinEditSectionLinks', [ $this, $nt, $section, $tooltip, &$links, $lang ] );
1535
1536
		$result = '<span class="mw-editsection"><span class="mw-editsection-bracket">[</span>';
1537
1538
		$linksHtml = [];
1539
		foreach ( $links as $k => $linkDetails ) {
1540
			$linksHtml[] = Linker::link(
1541
				$linkDetails['targetTitle'],
1542
				$linkDetails['text'],
1543
				$linkDetails['attribs'],
1544
				$linkDetails['query'],
1545
				$linkDetails['options']
1546
			);
1547
		}
1548
1549
		$result .= implode(
1550
			'<span class="mw-editsection-divider">'
1551
				. wfMessage( 'pipe-separator' )->inLanguage( $lang )->text()
1552
				. '</span>',
1553
			$linksHtml
1554
		);
1555
1556
		$result .= '<span class="mw-editsection-bracket">]</span></span>';
1557
		// Deprecated, use SkinEditSectionLinks hook instead
1558
		Hooks::run(
1559
			'DoEditSectionLink',
1560
			[ $this, $nt, $section, $tooltip, &$result, $lang ],
1561
			'1.25'
1562
		);
1563
		return $result;
1564
	}
1565
1566
	/** @deprecated in 1.21 */
1567
	public function commentBlock( $comment, $title = null, $local = false, $wikiId = null ) {
1568
		wfDeprecated( __METHOD__, '1.21' );
1569
		return Linker::commentBlock( $comment, $title, $local, $wikiId );
1570
	}
1571
1572
	/** @deprecated in 1.21 */
1573
	public function generateRollback(
1574
		$rev,
1575
		IContextSource $context = null,
1576
		$options = [ 'verify' ]
1577
	) {
1578
		wfDeprecated( __METHOD__, '1.21' );
1579
		return Linker::generateRollback( $rev, $context, $options );
1580
	}
1581
1582
	/** @deprecated in 1.21 */
1583
	public function link( $target, $html = null, $customAttribs = [], $query = [], $options = [] ) {
1584
		wfDeprecated( __METHOD__, '1.21' );
1585
		return Linker::link( $target, $html, $customAttribs, $query, $options );
1586
	}
1587
1588
	/** @deprecated in 1.21 */
1589
	public function linkKnown(
1590
		$target,
1591
		$html = null,
1592
		$customAttribs = [],
1593
		$query = [],
1594
		$options = [ 'known', 'noclasses' ]
1595
	) {
1596
		wfDeprecated( __METHOD__, '1.21' );
1597
		return Linker::linkKnown( $target, $html, $customAttribs, $query, $options );
1598
	}
1599
1600
	/** @deprecated in 1.21 */
1601
	public function userLink( $userId, $userName, $altUserName = false ) {
1602
		wfDeprecated( __METHOD__, '1.21' );
1603
		return Linker::userLink( $userId, $userName, $altUserName );
1604
	}
1605
1606
	/** @deprecated in 1.21 */
1607
	public function userToolLinks(
1608
		$userId,
1609
		$userText,
1610
		$redContribsWhenNoEdits = false,
1611
		$flags = 0,
1612
		$edits = null
1613
	) {
1614
		wfDeprecated( __METHOD__, '1.21' );
1615
		return Linker::userToolLinks( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1616
	}
1617
1618
}
1619