Completed
Branch master (939199)
by
unknown
39:35
created

includes/skins/Skin.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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