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