Completed
Branch master (d58858)
by
unknown
28:23
created

Skin::makeNSUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 3
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();
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
391
		return "$numeric $type $name";
392
	}
393
394
	/**
395
	 * Return values for <html> element
396
	 * @return array Array of associative name-to-value elements for <html> element
397
	 */
398
	public function getHtmlElementAttributes() {
399
		$lang = $this->getLanguage();
400
		return [
401
			'lang' => $lang->getHtmlCode(),
402
			'dir' => $lang->getDir(),
403
			'class' => 'client-nojs',
404
		];
405
	}
406
407
	/**
408
	 * This will be called by OutputPage::headElement when it is creating the
409
	 * "<body>" tag, skins can override it if they have a need to add in any
410
	 * body attributes or classes of their own.
411
	 * @param OutputPage $out
412
	 * @param array $bodyAttrs
413
	 */
414
	function addToBodyAttributes( $out, &$bodyAttrs ) {
415
		// does nothing by default
416
	}
417
418
	/**
419
	 * URL to the logo
420
	 * @return string
421
	 */
422
	function getLogo() {
423
		global $wgLogo;
424
		return $wgLogo;
425
	}
426
427
	/**
428
	 * @return string HTML
429
	 */
430
	function getCategoryLinks() {
431
		global $wgUseCategoryBrowser;
432
433
		$out = $this->getOutput();
434
		$allCats = $out->getCategoryLinks();
435
436
		if ( !count( $allCats ) ) {
437
			return '';
438
		}
439
440
		$embed = "<li>";
441
		$pop = "</li>";
442
443
		$s = '';
444
		$colon = $this->msg( 'colon-separator' )->escaped();
445
446
		if ( !empty( $allCats['normal'] ) ) {
447
			$t = $embed . implode( "{$pop}{$embed}", $allCats['normal'] ) . $pop;
448
449
			$msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped();
450
			$linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text();
451
			$title = Title::newFromText( $linkPage );
452
			$link = $title ? Linker::link( $title, $msg ) : $msg;
453
			$s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' .
454
				$link . $colon . '<ul>' . $t . '</ul>' . '</div>';
455
		}
456
457
		# Hidden categories
458
		if ( isset( $allCats['hidden'] ) ) {
459
			if ( $this->getUser()->getBoolOption( 'showhiddencats' ) ) {
460
				$class = ' mw-hidden-cats-user-shown';
461
			} elseif ( $this->getTitle()->getNamespace() == NS_CATEGORY ) {
462
				$class = ' mw-hidden-cats-ns-shown';
463
			} else {
464
				$class = ' mw-hidden-cats-hidden';
465
			}
466
467
			$s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" .
468
				$this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
469
				$colon . '<ul>' . $embed . implode( "{$pop}{$embed}", $allCats['hidden'] ) . $pop . '</ul>' .
470
				'</div>';
471
		}
472
473
		# optional 'dmoz-like' category browser. Will be shown under the list
474
		# of categories an article belong to
475
		if ( $wgUseCategoryBrowser ) {
476
			$s .= '<br /><hr />';
477
478
			# get a big array of the parents tree
479
			$parenttree = $this->getTitle()->getParentCategoryTree();
480
			# Skin object passed by reference cause it can not be
481
			# accessed under the method subfunction drawCategoryBrowser
482
			$tempout = explode( "\n", $this->drawCategoryBrowser( $parenttree ) );
483
			# Clean out bogus first entry and sort them
484
			unset( $tempout[0] );
485
			asort( $tempout );
486
			# Output one per line
487
			$s .= implode( "<br />\n", $tempout );
488
		}
489
490
		return $s;
491
	}
492
493
	/**
494
	 * Render the array as a series of links.
495
	 * @param array $tree Categories tree returned by Title::getParentCategoryTree
496
	 * @return string Separated by &gt;, terminate with "\n"
497
	 */
498
	function drawCategoryBrowser( $tree ) {
499
		$return = '';
500
501
		foreach ( $tree as $element => $parent ) {
502
			if ( empty( $parent ) ) {
503
				# element start a new list
504
				$return .= "\n";
505
			} else {
506
				# grab the others elements
507
				$return .= $this->drawCategoryBrowser( $parent ) . ' &gt; ';
508
			}
509
510
			# add our current element to the list
511
			$eltitle = Title::newFromText( $element );
512
			$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 511 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...
513
		}
514
515
		return $return;
516
	}
517
518
	/**
519
	 * @return string HTML
520
	 */
521
	function getCategories() {
522
		$out = $this->getOutput();
523
		$catlinks = $this->getCategoryLinks();
524
525
		// Check what we're showing
526
		$allCats = $out->getCategoryLinks();
527
		$showHidden = $this->getUser()->getBoolOption( 'showhiddencats' ) ||
528
						$this->getTitle()->getNamespace() == NS_CATEGORY;
529
530
		$classes = [ 'catlinks' ];
531
		if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
532
			$classes[] = 'catlinks-allhidden';
533
		}
534
535
		return Html::rawElement(
536
			'div',
537
			[ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
538
			$catlinks
539
		);
540
	}
541
542
	/**
543
	 * This runs a hook to allow extensions placing their stuff after content
544
	 * and article metadata (e.g. categories).
545
	 * Note: This function has nothing to do with afterContent().
546
	 *
547
	 * This hook is placed here in order to allow using the same hook for all
548
	 * skins, both the SkinTemplate based ones and the older ones, which directly
549
	 * use this class to get their data.
550
	 *
551
	 * The output of this function gets processed in SkinTemplate::outputPage() for
552
	 * the SkinTemplate based skins, all other skins should directly echo it.
553
	 *
554
	 * @return string Empty by default, if not changed by any hook function.
555
	 */
556
	protected function afterContentHook() {
557
		$data = '';
558
559
		if ( Hooks::run( 'SkinAfterContent', [ &$data, $this ] ) ) {
560
			// adding just some spaces shouldn't toggle the output
561
			// of the whole <div/>, so we use trim() here
562
			if ( trim( $data ) != '' ) {
563
				// Doing this here instead of in the skins to
564
				// ensure that the div has the same ID in all
565
				// skins
566
				$data = "<div id='mw-data-after-content'>\n" .
567
					"\t$data\n" .
568
					"</div>\n";
569
			}
570
		} else {
571
			wfDebug( "Hook SkinAfterContent changed output processing.\n" );
572
		}
573
574
		return $data;
575
	}
576
577
	/**
578
	 * Generate debug data HTML for displaying at the bottom of the main content
579
	 * area.
580
	 * @return string HTML containing debug data, if enabled (otherwise empty).
581
	 */
582
	protected function generateDebugHTML() {
583
		return MWDebug::getHTMLDebugLog();
584
	}
585
586
	/**
587
	 * This gets called shortly before the "</body>" tag.
588
	 *
589
	 * @return string HTML-wrapped JS code to be put before "</body>"
590
	 */
591
	function bottomScripts() {
592
		// TODO and the suckage continues. This function is really just a wrapper around
593
		// OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
594
		// up at some point
595
		$bottomScriptText = $this->getOutput()->getBottomScripts();
596
		Hooks::run( 'SkinAfterBottomScripts', [ $this, &$bottomScriptText ] );
597
598
		return $bottomScriptText;
599
	}
600
601
	/**
602
	 * Text with the permalink to the source page,
603
	 * usually shown on the footer of a printed page
604
	 *
605
	 * @return string HTML text with an URL
606
	 */
607
	function printSource() {
608
		$oldid = $this->getRevisionId();
609
		if ( $oldid ) {
610
			$canonicalUrl = $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid );
611
			$url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
612
		} else {
613
			// oldid not available for non existing pages
614
			$url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL() ) );
615
		}
616
617
		return $this->msg( 'retrievedfrom' )
618
			->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
619
			->parse();
620
	}
621
622
	/**
623
	 * @return string HTML
624
	 */
625
	function getUndeleteLink() {
626
		$action = $this->getRequest()->getVal( 'action', 'view' );
627
628
		if ( $this->getTitle()->userCan( 'deletedhistory', $this->getUser() ) &&
629
			( !$this->getTitle()->exists() || $action == 'history' ) ) {
630
			$n = $this->getTitle()->isDeleted();
631
632
			if ( $n ) {
633
				if ( $this->getTitle()->quickUserCan( 'undelete', $this->getUser() ) ) {
634
					$msg = 'thisisdeleted';
635
				} else {
636
					$msg = 'viewdeleted';
637
				}
638
639
				return $this->msg( $msg )->rawParams(
640
					Linker::linkKnown(
641
						SpecialPage::getTitleFor( 'Undelete', $this->getTitle()->getPrefixedDBkey() ),
642
						$this->msg( 'restorelink' )->numParams( $n )->escaped() )
643
					)->escaped();
644
			}
645
		}
646
647
		return '';
648
	}
649
650
	/**
651
	 * @return string
652
	 */
653
	function subPageSubtitle() {
654
		$out = $this->getOutput();
655
		$subpages = '';
656
657
		if ( !Hooks::run( 'SkinSubPageSubtitle', [ &$subpages, $this, $out ] ) ) {
658
			return $subpages;
659
		}
660
661
		if ( $out->isArticle() && MWNamespace::hasSubpages( $out->getTitle()->getNamespace() ) ) {
662
			$ptext = $this->getTitle()->getPrefixedText();
663
			if ( preg_match( '/\//', $ptext ) ) {
664
				$links = explode( '/', $ptext );
665
				array_pop( $links );
666
				$c = 0;
667
				$growinglink = '';
668
				$display = '';
669
				$lang = $this->getLanguage();
670
671
				foreach ( $links as $link ) {
672
					$growinglink .= $link;
673
					$display .= $link;
674
					$linkObj = Title::newFromText( $growinglink );
675
676
					if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
677
						$getlink = Linker::linkKnown(
678
							$linkObj,
679
							htmlspecialchars( $display )
680
						);
681
682
						$c++;
683
684
						if ( $c > 1 ) {
685
							$subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
686
						} else {
687
							$subpages .= '&lt; ';
688
						}
689
690
						$subpages .= $getlink;
691
						$display = '';
692
					} else {
693
						$display .= '/';
694
					}
695
					$growinglink .= '/';
696
				}
697
			}
698
		}
699
700
		return $subpages;
701
	}
702
703
	/**
704
	 * @deprecated since 1.27, feature removed
705
	 * @return bool Always false
706
	 */
707
	function showIPinHeader() {
708
		wfDeprecated( __METHOD__, '1.27' );
709
		return false;
710
	}
711
712
	/**
713
	 * @return string
714
	 */
715
	function getSearchLink() {
716
		$searchPage = SpecialPage::getTitleFor( 'Search' );
717
		return $searchPage->getLocalURL();
718
	}
719
720
	/**
721
	 * @return string
722
	 */
723
	function escapeSearchLink() {
724
		return htmlspecialchars( $this->getSearchLink() );
725
	}
726
727
	/**
728
	 * @param string $type
729
	 * @return string
730
	 */
731
	function getCopyright( $type = 'detect' ) {
732
		global $wgRightsPage, $wgRightsUrl, $wgRightsText;
733
734
		if ( $type == 'detect' ) {
735
			if ( !$this->isRevisionCurrent()
736
				&& !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
737
			) {
738
				$type = 'history';
739
			} else {
740
				$type = 'normal';
741
			}
742
		}
743
744
		if ( $type == 'history' ) {
745
			$msg = 'history_copyright';
746
		} else {
747
			$msg = 'copyright';
748
		}
749
750
		if ( $wgRightsPage ) {
751
			$title = Title::newFromText( $wgRightsPage );
752
			$link = Linker::linkKnown( $title, $wgRightsText );
753
		} elseif ( $wgRightsUrl ) {
754
			$link = Linker::makeExternalLink( $wgRightsUrl, $wgRightsText );
755
		} elseif ( $wgRightsText ) {
756
			$link = $wgRightsText;
757
		} else {
758
			# Give up now
759
			return '';
760
		}
761
762
		// Allow for site and per-namespace customization of copyright notice.
763
		// @todo Remove deprecated $forContent param from hook handlers and then remove here.
764
		$forContent = true;
765
766
		Hooks::run(
767
			'SkinCopyrightFooter',
768
			[ $this->getTitle(), $type, &$msg, &$link, &$forContent ]
769
		);
770
771
		return $this->msg( $msg )->rawParams( $link )->text();
772
	}
773
774
	/**
775
	 * @return null|string
776
	 */
777
	function getCopyrightIcon() {
778
		global $wgRightsUrl, $wgRightsText, $wgRightsIcon, $wgFooterIcons;
779
780
		$out = '';
781
782
		if ( $wgFooterIcons['copyright']['copyright'] ) {
783
			$out = $wgFooterIcons['copyright']['copyright'];
784
		} elseif ( $wgRightsIcon ) {
785
			$icon = htmlspecialchars( $wgRightsIcon );
786
787
			if ( $wgRightsUrl ) {
788
				$url = htmlspecialchars( $wgRightsUrl );
789
				$out .= '<a href="' . $url . '">';
790
			}
791
792
			$text = htmlspecialchars( $wgRightsText );
793
			$out .= "<img src=\"$icon\" alt=\"$text\" width=\"88\" height=\"31\" />";
794
795
			if ( $wgRightsUrl ) {
796
				$out .= '</a>';
797
			}
798
		}
799
800
		return $out;
801
	}
802
803
	/**
804
	 * Gets the powered by MediaWiki icon.
805
	 * @return string
806
	 */
807
	function getPoweredBy() {
808
		global $wgResourceBasePath;
809
810
		$url1 = htmlspecialchars(
811
			"$wgResourceBasePath/resources/assets/poweredby_mediawiki_88x31.png"
812
		);
813
		$url1_5 = htmlspecialchars(
814
			"$wgResourceBasePath/resources/assets/poweredby_mediawiki_132x47.png"
815
		);
816
		$url2 = htmlspecialchars(
817
			"$wgResourceBasePath/resources/assets/poweredby_mediawiki_176x62.png"
818
		);
819
		$text = '<a href="//www.mediawiki.org/"><img src="' . $url1
820
			. '" srcset="' . $url1_5 . ' 1.5x, ' . $url2 . ' 2x" '
821
			. 'height="31" width="88" alt="Powered by MediaWiki" /></a>';
822
		Hooks::run( 'SkinGetPoweredBy', [ &$text, $this ] );
823
		return $text;
824
	}
825
826
	/**
827
	 * Get the timestamp of the latest revision, formatted in user language
828
	 *
829
	 * @return string
830
	 */
831
	protected function lastModified() {
832
		$timestamp = $this->getOutput()->getRevisionTimestamp();
833
834
		# No cached timestamp, load it from the database
835
		if ( $timestamp === null ) {
836
			$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...
837
		}
838
839
		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...
840
			$d = $this->getLanguage()->userDate( $timestamp, $this->getUser() );
841
			$t = $this->getLanguage()->userTime( $timestamp, $this->getUser() );
842
			$s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->parse();
843
		} else {
844
			$s = '';
845
		}
846
847
		if ( wfGetLB()->getLaggedSlaveMode() ) {
848
			$s .= ' <strong>' . $this->msg( 'laggedslavemode' )->parse() . '</strong>';
849
		}
850
851
		return $s;
852
	}
853
854
	/**
855
	 * @param string $align
856
	 * @return string
857
	 */
858
	function logoText( $align = '' ) {
859
		if ( $align != '' ) {
860
			$a = " style='float: {$align};'";
861
		} else {
862
			$a = '';
863
		}
864
865
		$mp = $this->msg( 'mainpage' )->escaped();
866
		$mptitle = Title::newMainPage();
867
		$url = ( is_object( $mptitle ) ? htmlspecialchars( $mptitle->getLocalURL() ) : '' );
868
869
		$logourl = $this->getLogo();
870
		$s = "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
871
872
		return $s;
873
	}
874
875
	/**
876
	 * Renders a $wgFooterIcons icon according to the method's arguments
877
	 * @param array $icon The icon to build the html for, see $wgFooterIcons
878
	 *   for the format of this array.
879
	 * @param bool|string $withImage Whether to use the icon's image or output
880
	 *   a text-only footericon.
881
	 * @return string HTML
882
	 */
883
	function makeFooterIcon( $icon, $withImage = 'withImage' ) {
884
		if ( is_string( $icon ) ) {
885
			$html = $icon;
886
		} else { // Assuming array
887
			$url = isset( $icon["url"] ) ? $icon["url"] : null;
888
			unset( $icon["url"] );
889
			if ( isset( $icon["src"] ) && $withImage === 'withImage' ) {
890
				// do this the lazy way, just pass icon data as an attribute array
891
				$html = Html::element( 'img', $icon );
892
			} else {
893
				$html = htmlspecialchars( $icon["alt"] );
894
			}
895
			if ( $url ) {
896
				$html = Html::rawElement( 'a', [ "href" => $url ], $html );
897
			}
898
		}
899
		return $html;
900
	}
901
902
	/**
903
	 * Gets the link to the wiki's main page.
904
	 * @return string
905
	 */
906
	function mainPageLink() {
907
		$s = Linker::linkKnown(
908
			Title::newMainPage(),
909
			$this->msg( 'mainpage' )->escaped()
910
		);
911
912
		return $s;
913
	}
914
915
	/**
916
	 * Returns an HTML link for use in the footer
917
	 * @param string $desc The i18n message key for the link text
918
	 * @param string $page The i18n message key for the page to link to
919
	 * @return string HTML anchor
920
	 */
921
	public function footerLink( $desc, $page ) {
922
		// if the link description has been set to "-" in the default language,
923
		if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
924
			// then it is disabled, for all languages.
925
			return '';
926
		} else {
927
			// Otherwise, we display the link for the user, described in their
928
			// language (which may or may not be the same as the default language),
929
			// but we make the link target be the one site-wide page.
930
			$title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
931
932
			if ( !$title ) {
933
				return '';
934
			}
935
936
			return Linker::linkKnown(
937
				$title,
938
				$this->msg( $desc )->escaped()
939
			);
940
		}
941
	}
942
943
	/**
944
	 * Gets the link to the wiki's privacy policy page.
945
	 * @return string HTML
946
	 */
947
	function privacyLink() {
948
		return $this->footerLink( 'privacy', 'privacypage' );
949
	}
950
951
	/**
952
	 * Gets the link to the wiki's about page.
953
	 * @return string HTML
954
	 */
955
	function aboutLink() {
956
		return $this->footerLink( 'aboutsite', 'aboutpage' );
957
	}
958
959
	/**
960
	 * Gets the link to the wiki's general disclaimers page.
961
	 * @return string HTML
962
	 */
963
	function disclaimerLink() {
964
		return $this->footerLink( 'disclaimers', 'disclaimerpage' );
965
	}
966
967
	/**
968
	 * Return URL options for the 'edit page' link.
969
	 * This may include an 'oldid' specifier, if the current page view is such.
970
	 *
971
	 * @return array
972
	 * @private
973
	 */
974
	function editUrlOptions() {
975
		$options = [ 'action' => 'edit' ];
976
977
		if ( !$this->isRevisionCurrent() ) {
978
			$options['oldid'] = intval( $this->getRevisionId() );
979
		}
980
981
		return $options;
982
	}
983
984
	/**
985
	 * @param User|int $id
986
	 * @return bool
987
	 */
988
	function showEmailUser( $id ) {
989
		if ( $id instanceof User ) {
990
			$targetUser = $id;
991
		} else {
992
			$targetUser = User::newFromId( $id );
993
		}
994
995
		# The sending user must have a confirmed email address and the target
996
		# user must have a confirmed email address and allow emails from users.
997
		return $this->getUser()->canSendEmail() &&
998
			$targetUser->canReceiveEmail();
999
	}
1000
1001
	/**
1002
	 * Return a fully resolved style path url to images or styles stored in the current skins's folder.
1003
	 * This method returns a url resolved using the configured skin style path
1004
	 * and includes the style version inside of the url.
1005
	 *
1006
	 * Requires $stylename to be set, otherwise throws MWException.
1007
	 *
1008
	 * @param string $name The name or path of a skin resource file
1009
	 * @return string The fully resolved style path url including styleversion
1010
	 * @throws MWException
1011
	 */
1012
	function getSkinStylePath( $name ) {
1013
		global $wgStylePath, $wgStyleVersion;
1014
1015
		if ( $this->stylename === null ) {
1016
			$class = get_class( $this );
1017
			throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
1018
		}
1019
1020
		return "$wgStylePath/{$this->stylename}/$name?$wgStyleVersion";
1021
	}
1022
1023
	/* these are used extensively in SkinTemplate, but also some other places */
1024
1025
	/**
1026
	 * @param string $urlaction
1027
	 * @return string
1028
	 */
1029
	static function makeMainPageUrl( $urlaction = '' ) {
1030
		$title = Title::newMainPage();
1031
		self::checkTitle( $title, '' );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newMainPage() on line 1030 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...
1032
1033
		return $title->getLocalURL( $urlaction );
1034
	}
1035
1036
	/**
1037
	 * Make a URL for a Special Page using the given query and protocol.
1038
	 *
1039
	 * If $proto is set to null, make a local URL. Otherwise, make a full
1040
	 * URL with the protocol specified.
1041
	 *
1042
	 * @param string $name Name of the Special page
1043
	 * @param string $urlaction Query to append
1044
	 * @param string|null $proto Protocol to use or null for a local URL
1045
	 * @return string
1046
	 */
1047
	static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1048
		$title = SpecialPage::getSafeTitleFor( $name );
1049
		if ( is_null( $proto ) ) {
1050
			return $title->getLocalURL( $urlaction );
1051
		} else {
1052
			return $title->getFullURL( $urlaction, false, $proto );
1053
		}
1054
	}
1055
1056
	/**
1057
	 * @param string $name
1058
	 * @param string $subpage
1059
	 * @param string $urlaction
1060
	 * @return string
1061
	 */
1062
	static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1063
		$title = SpecialPage::getSafeTitleFor( $name, $subpage );
1064
		return $title->getLocalURL( $urlaction );
1065
	}
1066
1067
	/**
1068
	 * @param string $name
1069
	 * @param string $urlaction
1070
	 * @return string
1071
	 */
1072
	static function makeI18nUrl( $name, $urlaction = '' ) {
1073
		$title = Title::newFromText( wfMessage( $name )->inContentLanguage()->text() );
1074
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText(wfMe...tentLanguage()->text()) on line 1073 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...
1075
		return $title->getLocalURL( $urlaction );
1076
	}
1077
1078
	/**
1079
	 * @param string $name
1080
	 * @param string $urlaction
1081
	 * @return string
1082
	 */
1083
	static function makeUrl( $name, $urlaction = '' ) {
1084
		$title = Title::newFromText( $name );
1085
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($name) on line 1084 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...
1086
1087
		return $title->getLocalURL( $urlaction );
1088
	}
1089
1090
	/**
1091
	 * If url string starts with http, consider as external URL, else
1092
	 * internal
1093
	 * @param string $name
1094
	 * @return string URL
1095
	 */
1096
	static function makeInternalOrExternalUrl( $name ) {
1097
		if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
1098
			return $name;
1099
		} else {
1100
			return self::makeUrl( $name );
1101
		}
1102
	}
1103
1104
	/**
1105
	 * this can be passed the NS number as defined in Language.php
1106
	 * @param string $name
1107
	 * @param string $urlaction
1108
	 * @param int $namespace
1109
	 * @return string
1110
	 */
1111
	static function makeNSUrl( $name, $urlaction = '', $namespace = NS_MAIN ) {
1112
		$title = Title::makeTitleSafe( $namespace, $name );
1113
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $name) on line 1112 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...
1114
1115
		return $title->getLocalURL( $urlaction );
1116
	}
1117
1118
	/**
1119
	 * these return an array with the 'href' and boolean 'exists'
1120
	 * @param string $name
1121
	 * @param string $urlaction
1122
	 * @return array
1123
	 */
1124 View Code Duplication
	static function makeUrlDetails( $name, $urlaction = '' ) {
1125
		$title = Title::newFromText( $name );
1126
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($name) on line 1125 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...
1127
1128
		return [
1129
			'href' => $title->getLocalURL( $urlaction ),
1130
			'exists' => $title->isKnown(),
1131
		];
1132
	}
1133
1134
	/**
1135
	 * Make URL details where the article exists (or at least it's convenient to think so)
1136
	 * @param string $name Article name
1137
	 * @param string $urlaction
1138
	 * @return array
1139
	 */
1140 View Code Duplication
	static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1141
		$title = Title::newFromText( $name );
1142
		self::checkTitle( $title, $name );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($name) on line 1141 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...
1143
1144
		return [
1145
			'href' => $title->getLocalURL( $urlaction ),
1146
			'exists' => true
1147
		];
1148
	}
1149
1150
	/**
1151
	 * make sure we have some title to operate on
1152
	 *
1153
	 * @param Title $title
1154
	 * @param string $name
1155
	 */
1156
	static function checkTitle( &$title, $name ) {
1157
		if ( !is_object( $title ) ) {
1158
			$title = Title::newFromText( $name );
1159
			if ( !is_object( $title ) ) {
1160
				$title = Title::newFromText( '--error: link target missing--' );
1161
			}
1162
		}
1163
	}
1164
1165
	/**
1166
	 * Build an array that represents the sidebar(s), the navigation bar among them.
1167
	 *
1168
	 * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
1169
	 *
1170
	 * The format of the returned array is array( heading => content, ... ), where:
1171
	 * - heading is the heading of a navigation portlet. It is either:
1172
	 *   - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
1173
	 *   - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
1174
	 *   - plain text, which should be HTML-escaped by the skin
1175
	 * - content is the contents of the portlet. It is either:
1176
	 *   - HTML text (<ul><li>...</li>...</ul>)
1177
	 *   - array of link data in a format accepted by BaseTemplate::makeListItem()
1178
	 *   - (for a magic string as a key, any value)
1179
	 *
1180
	 * Note that extensions can control the sidebar contents using the SkinBuildSidebar hook
1181
	 * and can technically insert anything in here; skin creators are expected to handle
1182
	 * values described above.
1183
	 *
1184
	 * @return array
1185
	 */
1186
	function buildSidebar() {
1187
		global $wgEnableSidebarCache, $wgSidebarCacheExpiry;
1188
1189
		$that = $this;
1190
		$callback = function () use ( $that ) {
1191
			$bar = [];
1192
			$that->addToSidebar( $bar, 'sidebar' );
1193
			Hooks::run( 'SkinBuildSidebar', [ $that, &$bar ] );
1194
1195
			return $bar;
1196
		};
1197
1198
		if ( $wgEnableSidebarCache ) {
1199
			$cache = ObjectCache::getMainWANInstance();
1200
			$sidebar = $cache->getWithSetCallback(
1201
				$cache->makeKey( 'sidebar', $this->getLanguage()->getCode() ),
1202
				$wgSidebarCacheExpiry,
1203
				$callback,
1204
				[ 'lockTSE' => 30 ]
1205
			);
1206
		} else {
1207
			$sidebar = $callback();
1208
		}
1209
1210
		// Apply post-processing to the cached value
1211
		Hooks::run( 'SidebarBeforeOutput', [ $this, &$sidebar ] );
1212
1213
		return $sidebar;
1214
	}
1215
1216
	/**
1217
	 * Add content from a sidebar system message
1218
	 * Currently only used for MediaWiki:Sidebar (but may be used by Extensions)
1219
	 *
1220
	 * This is just a wrapper around addToSidebarPlain() for backwards compatibility
1221
	 *
1222
	 * @param array $bar
1223
	 * @param string $message
1224
	 */
1225
	public function addToSidebar( &$bar, $message ) {
1226
		$this->addToSidebarPlain( $bar, wfMessage( $message )->inContentLanguage()->plain() );
1227
	}
1228
1229
	/**
1230
	 * Add content from plain text
1231
	 * @since 1.17
1232
	 * @param array $bar
1233
	 * @param string $text
1234
	 * @return array
1235
	 */
1236
	function addToSidebarPlain( &$bar, $text ) {
1237
		$lines = explode( "\n", $text );
1238
1239
		$heading = '';
1240
1241
		foreach ( $lines as $line ) {
1242
			if ( strpos( $line, '*' ) !== 0 ) {
1243
				continue;
1244
			}
1245
			$line = rtrim( $line, "\r" ); // for Windows compat
1246
1247
			if ( strpos( $line, '**' ) !== 0 ) {
1248
				$heading = trim( $line, '* ' );
1249
				if ( !array_key_exists( $heading, $bar ) ) {
1250
					$bar[$heading] = [];
1251
				}
1252
			} else {
1253
				$line = trim( $line, '* ' );
1254
1255
				if ( strpos( $line, '|' ) !== false ) { // sanity check
1256
					$line = MessageCache::singleton()->transform( $line, false, null, $this->getTitle() );
1257
					$line = array_map( 'trim', explode( '|', $line, 2 ) );
1258
					if ( count( $line ) !== 2 ) {
1259
						// Second sanity check, could be hit by people doing
1260
						// funky stuff with parserfuncs... (bug 33321)
1261
						continue;
1262
					}
1263
1264
					$extraAttribs = [];
1265
1266
					$msgLink = $this->msg( $line[0] )->inContentLanguage();
1267
					if ( $msgLink->exists() ) {
1268
						$link = $msgLink->text();
1269
						if ( $link == '-' ) {
1270
							continue;
1271
						}
1272
					} else {
1273
						$link = $line[0];
1274
					}
1275
					$msgText = $this->msg( $line[1] );
1276
					if ( $msgText->exists() ) {
1277
						$text = $msgText->text();
1278
					} else {
1279
						$text = $line[1];
1280
					}
1281
1282
					if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
1283
						$href = $link;
1284
1285
						// Parser::getExternalLinkAttribs won't work here because of the Namespace things
1286
						global $wgNoFollowLinks, $wgNoFollowDomainExceptions;
1287
						if ( $wgNoFollowLinks && !wfMatchesDomainList( $href, $wgNoFollowDomainExceptions ) ) {
1288
							$extraAttribs['rel'] = 'nofollow';
1289
						}
1290
1291
						global $wgExternalLinkTarget;
1292
						if ( $wgExternalLinkTarget ) {
1293
							$extraAttribs['target'] = $wgExternalLinkTarget;
1294
						}
1295
					} else {
1296
						$title = Title::newFromText( $link );
1297
1298
						if ( $title ) {
1299
							$title = $title->fixSpecialName();
1300
							$href = $title->getLinkURL();
1301
						} else {
1302
							$href = 'INVALID-TITLE';
1303
						}
1304
					}
1305
1306
					$bar[$heading][] = array_merge( [
1307
						'text' => $text,
1308
						'href' => $href,
1309
						'id' => 'n-' . Sanitizer::escapeId( strtr( $line[1], ' ', '-' ), 'noninitial' ),
1310
						'active' => false
1311
					], $extraAttribs );
1312
				} else {
1313
					continue;
1314
				}
1315
			}
1316
		}
1317
1318
		return $bar;
1319
	}
1320
1321
	/**
1322
	 * Gets new talk page messages for the current user and returns an
1323
	 * appropriate alert message (or an empty string if there are no messages)
1324
	 * @return string
1325
	 */
1326
	function getNewtalks() {
1327
1328
		$newMessagesAlert = '';
1329
		$user = $this->getUser();
1330
		$newtalks = $user->getNewMessageLinks();
1331
		$out = $this->getOutput();
1332
1333
		// Allow extensions to disable or modify the new messages alert
1334
		if ( !Hooks::run( 'GetNewMessagesAlert', [ &$newMessagesAlert, $newtalks, $user, $out ] ) ) {
1335
			return '';
1336
		}
1337
		if ( $newMessagesAlert ) {
1338
			return $newMessagesAlert;
1339
		}
1340
1341
		if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
1342
			$uTalkTitle = $user->getTalkPage();
1343
			$lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null;
1344
			$nofAuthors = 0;
1345
			if ( $lastSeenRev !== null ) {
1346
				$plural = true; // Default if we have a last seen revision: if unknown, use plural
1347
				$latestRev = Revision::newFromTitle( $uTalkTitle, false, Revision::READ_NORMAL );
1348
				if ( $latestRev !== null ) {
1349
					// Singular if only 1 unseen revision, plural if several unseen revisions.
1350
					$plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1351
					$nofAuthors = $uTalkTitle->countAuthorsBetween(
1352
						$lastSeenRev, $latestRev, 10, 'include_new' );
1353
				}
1354
			} else {
1355
				// Singular if no revision -> diff link will show latest change only in any case
1356
				$plural = false;
1357
			}
1358
			$plural = $plural ? 999 : 1;
1359
			// 999 signifies "more than one revision". We don't know how many, and even if we did,
1360
			// the number of revisions or authors is not necessarily the same as the number of
1361
			// "messages".
1362
			$newMessagesLink = Linker::linkKnown(
1363
				$uTalkTitle,
1364
				$this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(),
1365
				[],
1366
				[ 'redirect' => 'no' ]
1367
			);
1368
1369
			$newMessagesDiffLink = Linker::linkKnown(
1370
				$uTalkTitle,
1371
				$this->msg( 'newmessagesdifflinkplural' )->params( $plural )->escaped(),
1372
				[],
1373
				$lastSeenRev !== null
1374
					? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1375
					: [ 'diff' => 'cur' ]
1376
			);
1377
1378
			if ( $nofAuthors >= 1 && $nofAuthors <= 10 ) {
1379
				$newMessagesAlert = $this->msg(
1380
					'youhavenewmessagesfromusers',
1381
					$newMessagesLink,
1382
					$newMessagesDiffLink
1383
				)->numParams( $nofAuthors, $plural );
1384
			} else {
1385
				// $nofAuthors === 11 signifies "11 or more" ("more than 10")
1386
				$newMessagesAlert = $this->msg(
1387
					$nofAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
1388
					$newMessagesLink,
1389
					$newMessagesDiffLink
1390
				)->numParams( $plural );
1391
			}
1392
			$newMessagesAlert = $newMessagesAlert->text();
1393
			# Disable CDN cache
1394
			$out->setCdnMaxage( 0 );
1395
		} elseif ( count( $newtalks ) ) {
1396
			$sep = $this->msg( 'newtalkseparator' )->escaped();
1397
			$msgs = [];
1398
1399
			foreach ( $newtalks as $newtalk ) {
1400
				$msgs[] = Xml::element(
1401
					'a',
1402
					[ 'href' => $newtalk['link'] ], $newtalk['wiki']
1403
				);
1404
			}
1405
			$parts = implode( $sep, $msgs );
1406
			$newMessagesAlert = $this->msg( 'youhavenewmessagesmulti' )->rawParams( $parts )->escaped();
1407
			$out->setCdnMaxage( 0 );
1408
		}
1409
1410
		return $newMessagesAlert;
1411
	}
1412
1413
	/**
1414
	 * Get a cached notice
1415
	 *
1416
	 * @param string $name Message name, or 'default' for $wgSiteNotice
1417
	 * @return string|bool HTML fragment, or false to indicate that the caller
1418
	 *   should fall back to the next notice in its sequence
1419
	 */
1420
	private function getCachedNotice( $name ) {
1421
		global $wgRenderHashAppend, $parserMemc, $wgContLang;
1422
1423
		$needParse = false;
1424
1425
		if ( $name === 'default' ) {
1426
			// special case
1427
			global $wgSiteNotice;
1428
			$notice = $wgSiteNotice;
1429
			if ( empty( $notice ) ) {
1430
				return false;
1431
			}
1432
		} else {
1433
			$msg = $this->msg( $name )->inContentLanguage();
1434
			if ( $msg->isBlank() ) {
1435
				return '';
1436
			} elseif ( $msg->isDisabled() ) {
1437
				return false;
1438
			}
1439
			$notice = $msg->plain();
1440
		}
1441
1442
		// Use the extra hash appender to let eg SSL variants separately cache.
1443
		$key = wfMemcKey( $name . $wgRenderHashAppend );
1444
		$cachedNotice = $parserMemc->get( $key );
1445
		if ( is_array( $cachedNotice ) ) {
1446
			if ( md5( $notice ) == $cachedNotice['hash'] ) {
1447
				$notice = $cachedNotice['html'];
1448
			} else {
1449
				$needParse = true;
1450
			}
1451
		} else {
1452
			$needParse = true;
1453
		}
1454
1455
		if ( $needParse ) {
1456
			$parsed = $this->getOutput()->parse( $notice );
1457
			$parserMemc->set( $key, [ 'html' => $parsed, 'hash' => md5( $notice ) ], 600 );
1458
			$notice = $parsed;
1459
		}
1460
1461
		$notice = Html::rawElement( 'div', [ 'id' => 'localNotice',
1462
			'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ], $notice );
1463
		return $notice;
1464
	}
1465
1466
	/**
1467
	 * Get the site notice
1468
	 *
1469
	 * @return string HTML fragment
1470
	 */
1471
	function getSiteNotice() {
1472
		$siteNotice = '';
1473
1474
		if ( Hooks::run( 'SiteNoticeBefore', [ &$siteNotice, $this ] ) ) {
1475
			if ( is_object( $this->getUser() ) && $this->getUser()->isLoggedIn() ) {
1476
				$siteNotice = $this->getCachedNotice( 'sitenotice' );
1477
			} else {
1478
				$anonNotice = $this->getCachedNotice( 'anonnotice' );
1479
				if ( $anonNotice === false ) {
1480
					$siteNotice = $this->getCachedNotice( 'sitenotice' );
1481
				} else {
1482
					$siteNotice = $anonNotice;
1483
				}
1484
			}
1485
			if ( $siteNotice === false ) {
1486
				$siteNotice = $this->getCachedNotice( 'default' );
1487
			}
1488
		}
1489
1490
		Hooks::run( 'SiteNoticeAfter', [ &$siteNotice, $this ] );
1491
		return $siteNotice;
1492
	}
1493
1494
	/**
1495
	 * Create a section edit link.  This supersedes editSectionLink() and
1496
	 * editSectionLinkForOther().
1497
	 *
1498
	 * @param Title $nt The title being linked to (may not be the same as
1499
	 *   the current page, if the section is included from a template)
1500
	 * @param string $section The designation of the section being pointed to,
1501
	 *   to be included in the link, like "&section=$section"
1502
	 * @param string $tooltip The tooltip to use for the link: will be escaped
1503
	 *   and wrapped in the 'editsectionhint' message
1504
	 * @param string $lang Language code
1505
	 * @return string HTML to use for edit link
1506
	 */
1507
	public function doEditSectionLink( Title $nt, $section, $tooltip = null, $lang = false ) {
1508
		// HTML generated here should probably have userlangattributes
1509
		// added to it for LTR text on RTL pages
1510
1511
		$lang = wfGetLangObj( $lang );
1512
1513
		$attribs = [];
1514
		if ( !is_null( $tooltip ) ) {
1515
			# Bug 25462: undo double-escaping.
1516
			$tooltip = Sanitizer::decodeCharReferences( $tooltip );
1517
			$attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
1518
				->inLanguage( $lang )->text();
1519
		}
1520
1521
		$links = [
1522
			'editsection' => [
1523
				'text' => wfMessage( 'editsection' )->inLanguage( $lang )->escaped(),
1524
				'targetTitle' => $nt,
1525
				'attribs' => $attribs,
1526
				'query' => [ 'action' => 'edit', 'section' => $section ],
1527
				'options' => [ 'noclasses', 'known' ]
1528
			]
1529
		];
1530
1531
		Hooks::run( 'SkinEditSectionLinks', [ $this, $nt, $section, $tooltip, &$links, $lang ] );
1532
1533
		$result = '<span class="mw-editsection"><span class="mw-editsection-bracket">[</span>';
1534
1535
		$linksHtml = [];
1536
		foreach ( $links as $k => $linkDetails ) {
1537
			$linksHtml[] = Linker::link(
1538
				$linkDetails['targetTitle'],
1539
				$linkDetails['text'],
1540
				$linkDetails['attribs'],
1541
				$linkDetails['query'],
1542
				$linkDetails['options']
1543
			);
1544
		}
1545
1546
		$result .= implode(
1547
			'<span class="mw-editsection-divider">'
1548
				. wfMessage( 'pipe-separator' )->inLanguage( $lang )->text()
1549
				. '</span>',
1550
			$linksHtml
1551
		);
1552
1553
		$result .= '<span class="mw-editsection-bracket">]</span></span>';
1554
		// Deprecated, use SkinEditSectionLinks hook instead
1555
		Hooks::run(
1556
			'DoEditSectionLink',
1557
			[ $this, $nt, $section, $tooltip, &$result, $lang ],
1558
			'1.25'
1559
		);
1560
		return $result;
1561
	}
1562
1563
	/**
1564
	 * Use PHP's magic __call handler to intercept legacy calls to the linker
1565
	 * for backwards compatibility.
1566
	 *
1567
	 * @param string $fname Name of called method
1568
	 * @param array $args Arguments to the method
1569
	 * @throws MWException
1570
	 * @return mixed
1571
	 */
1572
	function __call( $fname, $args ) {
1573
		$realFunction = [ 'Linker', $fname ];
1574
		if ( is_callable( $realFunction ) ) {
1575
			wfDeprecated( get_class( $this ) . '::' . $fname, '1.21' );
1576
			return call_user_func_array( $realFunction, $args );
1577
		} else {
1578
			$className = get_class( $this );
1579
			throw new MWException( "Call to undefined method $className::$fname" );
1580
		}
1581
	}
1582
1583
}
1584