Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.93% covered (warning)
56.93%
616 / 1082
43.28% covered (danger)
43.28%
29 / 67
CRAP
0.00% covered (danger)
0.00%
0 / 1
Skin
56.93% covered (warning)
56.93%
616 / 1082
43.28% covered (danger)
43.28%
29 / 67
6681.07
0.00% covered (danger)
0.00%
0 / 1
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getComponent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplateData
65.96% covered (warning)
65.96%
31 / 47
0.00% covered (danger)
0.00%
0 / 1
13.95
 normalizeKey
50.00% covered (danger)
50.00%
12 / 24
0.00% covered (danger)
0.00%
0 / 1
13.12
 __construct
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 getSkinName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isResponsive
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 initPage
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 getDefaultModules
71.19% covered (warning)
71.19%
42 / 59
0.00% covered (danger)
0.00%
0 / 1
27.64
 preloadExistence
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 setRelevantTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelevantTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRelevantUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelevantUser
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 outputPageFinal
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 outputPage
n/a
0 / 0
n/a
0 / 0
0
 getPageClasses
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 getHtmlElementAttributes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getCategoryLinks
16.28% covered (danger)
16.28%
7 / 43
0.00% covered (danger)
0.00%
0 / 1
35.75
 getCategories
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 afterContentHook
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getCanonicalUrl
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 printSource
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getUndeleteLink
18.18% covered (danger)
18.18%
6 / 33
0.00% covered (danger)
0.00%
0 / 1
43.05
 subPageSubtitleInternal
27.27% covered (danger)
27.27%
9 / 33
0.00% covered (danger)
0.00%
0 / 1
40.16
 getFooterTemplateDataItem
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getCopyright
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logoText
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 getFooterIcons
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 makeFooterIcon
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 editUrlOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 showEmailUser
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 makeMainPageUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 makeSpecialUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 makeSpecialUrlSubpage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 makeInternalOrExternalUrl
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 makeUrlDetails
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 makeKnownUrlDetails
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 mapInterwikiToLanguage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLanguages
71.43% covered (warning)
71.43%
45 / 63
0.00% covered (danger)
0.00%
0 / 1
13.82
 buildNavUrls
63.72% covered (warning)
63.72%
72 / 113
0.00% covered (danger)
0.00%
0 / 1
45.12
 buildFeedUrls
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
4.54
 buildSidebar
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
4
 addToSidebar
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addToSidebarPlain
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
1 / 1
16
 getSidebarIcon
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 hideNewTalkMessagesForCurrentSession
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNewtalks
2.50% covered (danger)
2.50%
2 / 80
0.00% covered (danger)
0.00%
0 / 1
223.54
 getCachedNotice
32.26% covered (danger)
32.26%
10 / 31
0.00% covered (danger)
0.00%
0 / 1
12.77
 getSiteNotice
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
11.35
 doEditSectionLink
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
6
 doEditSectionLinksHTML
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 makeToolbox
61.11% covered (warning)
61.11%
22 / 36
0.00% covered (danger)
0.00%
0 / 1
15.88
 getIndicatorsData
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getPersonalToolsForMakeListItem
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
9
 makeLink
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 makeListItem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getAfterPortlet
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prepareSubtitle
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getJsConfigVars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserLanguageAttributes
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
3.43
 prepareUserLanguageAttributes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prepareUndeleteLink
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 wrapHTML
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
1
 supportsMenu
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getPortletLinkOptions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getPortletData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\Context\ContextSource;
22use MediaWiki\Debug\MWDebug;
23use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
24use MediaWiki\Html\Html;
25use MediaWiki\Language\Language;
26use MediaWiki\Language\LanguageCode;
27use MediaWiki\Logger\LoggerFactory;
28use MediaWiki\MainConfigNames;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Output\OutputPage;
31use MediaWiki\Parser\Sanitizer;
32use MediaWiki\ResourceLoader as RL;
33use MediaWiki\Revision\RevisionStore;
34use MediaWiki\Skin\SkinComponent;
35use MediaWiki\Skin\SkinComponentFooter;
36use MediaWiki\Skin\SkinComponentLink;
37use MediaWiki\Skin\SkinComponentListItem;
38use MediaWiki\Skin\SkinComponentMenu;
39use MediaWiki\Skin\SkinComponentRegistry;
40use MediaWiki\Skin\SkinComponentRegistryContext;
41use MediaWiki\Skin\SkinComponentUtils;
42use MediaWiki\SpecialPage\SpecialPage;
43use MediaWiki\Specials\SpecialUserRights;
44use MediaWiki\Title\Title;
45use MediaWiki\Title\TitleValue;
46use MediaWiki\User\User;
47use MediaWiki\User\UserIdentity;
48use MediaWiki\User\UserIdentityValue;
49use MediaWiki\WikiMap\WikiMap;
50use Wikimedia\ObjectCache\WANObjectCache;
51use Wikimedia\Rdbms\IDBAccessObject;
52
53/**
54 * @defgroup Skins Skins
55 */
56
57/**
58 * The base class for all skins.
59 *
60 * See docs/Skin.md for more information.
61 *
62 * @stable to extend
63 * @ingroup Skins
64 */
65abstract class Skin extends ContextSource {
66    use ProtectedHookAccessorTrait;
67
68    /**
69     * @var array link options used in Skin::makeLink. Can be set by skin option `link`.
70     */
71    private $defaultLinkOptions;
72
73    /**
74     * @var string|null
75     */
76    protected $skinname = null;
77
78    /**
79     * @var array Skin options passed into constructor
80     */
81    protected $options = [];
82    /** @var Title|null */
83    protected $mRelevantTitle = null;
84
85    /**
86     * @var UserIdentity|null|false
87     */
88    private $mRelevantUser = false;
89
90    /** The current major version of the skin specification. */
91    protected const VERSION_MAJOR = 1;
92
93    /** @var array|null Cache for getLanguages() */
94    private $languageLinks;
95
96    /** @var array|null Cache for buildSidebar() */
97    private $sidebar;
98
99    /**
100     * @var SkinComponentRegistry Initialised in constructor.
101     */
102    private $componentRegistry = null;
103
104    /**
105     * Get the current major version of Skin. This is used to manage changes
106     * to underlying data and for providing support for older and new versions of code.
107     *
108     * @since 1.36
109     * @return int
110     */
111    public static function getVersion() {
112        return self::VERSION_MAJOR;
113    }
114
115    /**
116     * @internal use in Skin.php, SkinTemplate.php or SkinMustache.php
117     * @param string $name
118     * @return SkinComponent
119     */
120    final protected function getComponent( string $name ): SkinComponent {
121        return $this->componentRegistry->getComponent( $name );
122    }
123
124    /**
125     * @stable to extend. Subclasses may extend this method to add additional
126     * template data.
127     * @internal this method should never be called outside Skin and its subclasses
128     * as it can be computationally expensive and typically has side effects on the Skin
129     * instance, through execution of hooks.
130     *
131     * The data keys should be valid English words. Compound words should
132     * be hyphenated except if they are normally written as one word. Each
133     * key should be prefixed with a type hint, this may be enforced by the
134     * class PHPUnit test.
135     *
136     * Plain strings are prefixed with 'html-', plain arrays with 'array-'
137     * and complex array data with 'data-'. 'is-' and 'has-' prefixes can
138     * be used for boolean variables.
139     * Messages are prefixed with 'msg-', followed by their message key.
140     * All messages specified in the skin option 'messages' will be available
141     * under 'msg-MESSAGE-NAME'.
142     *
143     * @return array Data for a mustache template
144     */
145    public function getTemplateData() {
146        $title = $this->getTitle();
147        $out = $this->getOutput();
148        $user = $this->getUser();
149        $isMainPage = $title->isMainPage();
150        $blankedHeading = false;
151        // Heading can only be blanked on "views". It should
152        // still show on action=edit, diff pages and action=history
153        $isHeadingOverridable = $this->getContext()->getActionName() === 'view' &&
154            !$this->getRequest()->getRawVal( 'diff' );
155
156        if ( $isMainPage && $isHeadingOverridable ) {
157            // Special casing for the main page to allow more freedom to editors, to
158            // design their home page differently. This came up in T290480.
159            // The parameter for logged in users is optional and may
160            // or may not be used.
161            $titleMsg = $user->isAnon() ?
162                $this->msg( 'mainpage-title' ) :
163                $this->msg( 'mainpage-title-loggedin', $user->getName() );
164
165            // T298715: Use content language rather than user language so that
166            // the custom page heading is shown to all users, not just those that have
167            // their interface set to the site content language.
168            //
169            // T331095: Avoid Message::inContentLanuguage and, just like Parser,
170            // pick the language variant based on the current URL and/or user
171            // preference if their variant relates to the content language.
172            $forceUIMsgAsContentMsg = $this->getConfig()
173                ->get( MainConfigNames::ForceUIMsgAsContentMsg );
174            if ( !in_array( $titleMsg->getKey(), (array)$forceUIMsgAsContentMsg ) ) {
175                $services = MediaWikiServices::getInstance();
176                $contLangVariant = $services->getLanguageConverterFactory()
177                    ->getLanguageConverter( $services->getContentLanguage() )
178                    ->getPreferredVariant();
179                $titleMsg->inLanguage( $contLangVariant );
180            }
181            $titleMsg->setInterfaceMessageFlag( true );
182            $blankedHeading = $titleMsg->isBlank();
183            if ( !$titleMsg->isDisabled() ) {
184                $htmlTitle = $titleMsg->parse();
185            } else {
186                $htmlTitle = $out->getPageTitle();
187            }
188        } else {
189            $htmlTitle = $out->getPageTitle();
190        }
191
192        $data = [
193            // raw HTML
194            'html-title-heading' => Html::rawElement(
195                'h1',
196                [
197                    'id' => 'firstHeading',
198                    'class' => 'firstHeading mw-first-heading',
199                    'style' => $blankedHeading ? 'display: none' : null
200                ] + $this->getUserLanguageAttributes(),
201                $htmlTitle
202            ),
203            'html-title' => $htmlTitle ?: null,
204            // Boolean values
205            'is-title-blank' => $blankedHeading, // @since 1.38
206            'is-anon' => $user->isAnon(),
207            'is-article' => $out->isArticle(),
208            'is-mainpage' => $isMainPage,
209            'is-specialpage' => $title->isSpecialPage(),
210            'canonical-url' => $this->getCanonicalUrl(),
211        ];
212
213        $components = $this->componentRegistry->getComponents();
214        foreach ( $components as $componentName => $component ) {
215            $data['data-' . $componentName] = $component->getTemplateData();
216        }
217        return $data;
218    }
219
220    /**
221     * Normalize a skin preference value to a form that can be loaded.
222     *
223     * If a skin can't be found, it will fall back to the configured default ($wgDefaultSkin), or the
224     * hardcoded default ($wgFallbackSkin) if the default skin is unavailable too.
225     *
226     * @param string $key 'monobook', 'vector', etc.
227     * @return string
228     */
229    public static function normalizeKey( string $key ) {
230        $config = MediaWikiServices::getInstance()->getMainConfig();
231        $defaultSkin = $config->get( MainConfigNames::DefaultSkin );
232        $fallbackSkin = $config->get( MainConfigNames::FallbackSkin );
233        $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
234        $skinNames = $skinFactory->getInstalledSkins();
235
236        // Make keys lowercase for case-insensitive matching.
237        $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
238        $key = strtolower( $key );
239        $defaultSkin = strtolower( $defaultSkin );
240        $fallbackSkin = strtolower( $fallbackSkin );
241
242        if ( $key == '' || $key == 'default' ) {
243            // Don't return the default immediately;
244            // in a misconfiguration we need to fall back.
245            $key = $defaultSkin;
246        }
247
248        if ( isset( $skinNames[$key] ) ) {
249            return $key;
250        }
251
252        // Older versions of the software used a numeric setting
253        // in the user preferences.
254        $fallback = [
255            0 => $defaultSkin,
256            2 => 'cologneblue'
257        ];
258
259        if ( isset( $fallback[$key] ) ) {
260            // @phan-suppress-next-line PhanTypeMismatchDimFetch False positive
261            $key = $fallback[$key];
262        }
263
264        if ( isset( $skinNames[$key] ) ) {
265            return $key;
266        } elseif ( isset( $skinNames[$defaultSkin] ) ) {
267            return $defaultSkin;
268        } else {
269            return $fallbackSkin;
270        }
271    }
272
273    /**
274     * @since 1.31
275     * @param string|array|null $options Options for the skin can be an array (since 1.35).
276     *  When a string is passed, it's the skinname.
277     *  When an array is passed:
278     *
279     *  - `name`: Required. Internal skin name, generally in lowercase to comply with conventions
280     *     for interface message keys and CSS class names which embed this value.
281     *
282     *  - `format`: Enable rendering of skin as json or html.
283     *
284     *     Since: MW 1.43
285     *     Default: `html`
286     *
287     *  - `styles`: ResourceLoader style modules to load on all pages. Default: `[]`
288     *
289     *  - `scripts`: ResourceLoader script modules to load on all pages. Default: `[]`
290     *
291     *  - `toc`: Whether a table of contents is included in the main article content
292     *     area. If your skin has place a table of contents elsewhere (for example, the sidebar),
293     *     set this to `false`.
294     *
295     *     See ParserOutput::getText() for the implementation logic.
296     *
297     *     Default: `true`
298     *
299     *  - `bodyClasses`: An array of extra class names to add to the HTML `<body>` element.
300     *     Default: `[]`
301     *
302     *  - `clientPrefEnabled`: Enable support for mw.user.clientPrefs.
303     *     This instructs OutputPage and ResourceLoader\ClientHtml to include an inline script
304     *     in web responses for unregistered users to switch HTML classes as needed.
305     *
306     *     Since: MW 1.41
307     *     Default: `false`
308     *
309     *  - `wrapSiteNotice`: Enable support for standard site notice wrapper.
310     *     This instructs the Skin to wrap banners in div#siteNotice.
311     *
312     *     Since: MW 1.42
313     *     Default: `false`
314     *
315     *  - `responsive`: Whether the skin supports responsive behaviour and wants a viewport meta
316     *     tag to be added to the HTML head. Note, users can disable this feature via a user
317     *     preference.
318     *
319     *     Default: `false`
320     *
321     *  - `supportsMwHeading`: Whether the skin supports new HTML markup for headings, which uses
322     *     `<div class="mw-heading">` tags (https://www.mediawiki.org/wiki/Heading_HTML_changes).
323     *     If false, MediaWiki will output the legacy markup instead.
324     *
325     *     Since: MW 1.43
326     *     Default: `false` (will become `true` in and then will be removed in the future)
327     *
328     *  - `link`: An array of link option overriddes. See Skin::makeLink for the available options.
329     *
330     *     Default: `[]`
331     *
332     *  - `tempUserBanner`: Whether to display a banner on page views by in temporary user sessions.
333     *     This will prepend SkinComponentTempUserBanner to the `<body>` above everything else.
334     *     See also MediaWiki\MainConfigSchema::AutoCreateTempUser and User::isTemp.
335     *
336     *     Default: `false`
337     *
338     *  - `menus`: Which menus the skin supports, to allow features like SpecialWatchlist
339     *     to render their own navigation in the skins that don't support certain menus.
340     *     For any key in the array, the skin is promising to render an element e.g. the
341     *     presence of `associated-pages` means the skin will render a menu
342     *     compatible with mw.util.addPortletLink which has the ID p-associated-pages.
343     *
344     *     Default: `['namespaces', 'views', 'actions', 'variants']`
345     *
346     *     Opt-in menus:
347     *     - `associated-pages`
348     *     - `notifications`
349     *     - `user-interface-preferences`
350     *     - `user-page`
351     *     - `user-menu`
352     */
353    public function __construct( $options = null ) {
354        if ( is_string( $options ) ) {
355            $this->skinname = $options;
356        } elseif ( $options ) {
357            $name = $options['name'] ?? null;
358
359            if ( !$name ) {
360                throw new SkinException( 'Skin name must be specified' );
361            }
362
363            // Defaults are set in Skin::getOptions()
364            $this->options = $options;
365            $this->skinname = $name;
366        }
367        $this->defaultLinkOptions = $this->getOptions()['link'];
368        $this->componentRegistry = new SkinComponentRegistry(
369            new SkinComponentRegistryContext( $this )
370        );
371    }
372
373    /**
374     * @return string|null Skin name
375     */
376    public function getSkinName() {
377        return $this->skinname;
378    }
379
380    /**
381     * Indicates if this skin is responsive.
382     * Responsive skins have skin--responsive added to <body> by OutputPage,
383     * and a viewport <meta> tag set by Skin::initPage.
384     *
385     * @since 1.36
386     * @stable to override
387     * @return bool
388     */
389    public function isResponsive() {
390        $isSkinResponsiveCapable = $this->getOptions()['responsive'];
391        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
392
393        return $isSkinResponsiveCapable &&
394            $userOptionsLookup->getBoolOption( $this->getUser(), 'skin-responsive' );
395    }
396
397    /**
398     * @stable to override
399     * @param OutputPage $out
400     */
401    public function initPage( OutputPage $out ) {
402        $skinMetaTags = $this->getConfig()->get( MainConfigNames::SkinMetaTags );
403        $siteName = $this->getConfig()->get( MainConfigNames::Sitename );
404        $this->preloadExistence();
405
406        if ( $this->isResponsive() ) {
407            $out->addMeta(
408                'viewport',
409                'width=device-width, initial-scale=1.0, ' .
410                'user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0'
411            );
412        } else {
413            // Force the desktop experience on an iPad by resizing the mobile viewport to
414            // the value of @min-width-breakpoint-desktop (1120px).
415            // This is as @min-width-breakpoint-desktop-wide usually tends to optimize
416            // for larger screens with max-widths and margins.
417            // The initial-scale SHOULD NOT be set here as defining it will impact zoom
418            // on mobile devices. To allow font-size adjustment in iOS devices (see T311795)
419            // we will define a zoom in JavaScript on certain devices (see resources/src/mediawiki.page.ready/ready.js)
420            $out->addMeta(
421                'viewport',
422                'width=1120'
423            );
424        }
425
426        $tags = [
427            'og:site_name' => $siteName,
428            'og:title' => $out->getHTMLTitle(),
429            'twitter:card' => 'summary_large_image',
430            'og:type' => 'website',
431        ];
432
433        // Support sharing on platforms such as Facebook and Twitter
434        foreach ( $tags as $key => $value ) {
435            if ( in_array( $key, $skinMetaTags ) ) {
436                $out->addMeta( $key, $value );
437            }
438        }
439    }
440
441    /**
442     * Defines the ResourceLoader modules that should be added to the skin
443     * It is recommended that skins wishing to override call parent::getDefaultModules()
444     * and substitute out any modules they wish to change by using a key to look them up
445     *
446     * Any modules defined with the 'styles' key will be added as render blocking CSS via
447     * Output::addModuleStyles. Similarly, each key should refer to a list of modules
448     *
449     * @stable to override
450     * @return array Array of modules with helper keys for easy overriding
451     */
452    public function getDefaultModules() {
453        $out = $this->getOutput();
454        $user = $this->getUser();
455
456        // Modules declared in the $modules literal are loaded
457        // for ALL users, on ALL pages, in ALL skins.
458        // Keep this list as small as possible!
459        $modules = [
460            // The 'styles' key sets render-blocking style modules
461            // Unlike other keys in $modules, this is an associative array
462            // where each key is its own group pointing to a list of modules
463            'styles' => [
464                'skin' => $this->getOptions()['styles'],
465                'core' => [],
466                'content' => [],
467                'syndicate' => [],
468                'user' => []
469            ],
470            'core' => [
471                'site',
472                'mediawiki.page.ready',
473            ],
474            // modules that enhance the content in some way
475            'content' => [],
476            // modules relating to search functionality
477            'search' => [],
478            // Skins can register their own scripts
479            'skin' => $this->getOptions()['scripts'],
480            // modules relating to functionality relating to watching an article
481            'watch' => [],
482            // modules which relate to the current users preferences
483            'user' => [],
484            // modules relating to RSS/Atom Feeds
485            'syndicate' => [],
486        ];
487
488        // Preload jquery.tablesorter for mediawiki.page.ready
489        if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
490            $modules['content'][] = 'jquery.tablesorter';
491            $modules['styles']['content'][] = 'jquery.tablesorter.styles';
492        }
493
494        // Preload jquery.makeCollapsible for mediawiki.page.ready
495        if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
496            $modules['content'][] = 'jquery.makeCollapsible';
497            $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
498        }
499
500        // Load relevant styles on wiki pages that use mw-ui-button.
501        // Since 1.26, this no longer loads unconditionally. Special pages
502        // and extensions should load this via addModuleStyles() instead.
503        if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
504            $modules['styles']['content'][] = 'mediawiki.ui.button';
505        }
506        // Since 1.41, styling for mw-message-box is only required for
507        // messages that appear in article content.
508        // This should only be removed when a suitable alternative exists
509        // e.g. https://phabricator.wikimedia.org/T363607 is resolved.
510        if ( strpos( $out->getHTML(), 'mw-message-box' ) !== false ) {
511            $modules['styles']['content'][] = 'mediawiki.legacy.messageBox';
512        }
513        // If the page is using Codex message box markup load Codex styles.
514        // Since 1.41. Skins can unset this if they prefer to handle this via other
515        // means.
516        // This is intended for extensions.
517        // For content, this should not be considered stable, and will likely
518        // be removed when https://phabricator.wikimedia.org/T363607 is resolved.
519        if ( strpos( $out->getHTML(), 'cdx-message' ) !== false ) {
520            // This channel will be used to identify pages relying on this method that
521            // shouldn't be.
522            $logger = LoggerFactory::getInstance( 'SkinCodex' );
523            $codexModules = array_filter( $out->getModules(), static function ( $module ) {
524                return strpos( $module, 'codex' ) !== false;
525            } );
526            if ( !$codexModules ) {
527                $logger->warning( 'Page uses Codex markup without appropriate style pack.' );
528                $modules['styles']['content'][] = 'mediawiki.codex.messagebox.styles';
529            }
530        }
531
532        if ( $out->isTOCEnabled() ) {
533            $modules['content'][] = 'mediawiki.toc';
534        }
535
536        $authority = $this->getAuthority();
537        $relevantTitle = $this->getRelevantTitle();
538        if ( $authority->getUser()->isRegistered()
539            && $authority->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' )
540            && $relevantTitle && $relevantTitle->canExist()
541        ) {
542            $modules['watch'][] = 'mediawiki.page.watch.ajax';
543        }
544
545        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
546        if ( $userOptionsLookup->getBoolOption( $user, 'editsectiononrightclick' )
547            || ( $out->isArticle() && $userOptionsLookup->getOption( $user, 'editondblclick' ) )
548        ) {
549            $modules['user'][] = 'mediawiki.misc-authed-pref';
550        }
551
552        if ( $out->isSyndicated() ) {
553            $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
554        }
555
556        if ( $user->isTemp() ) {
557            $modules['user'][] = 'mediawiki.tempUserBanner';
558            $modules['styles']['user'][] = 'mediawiki.tempUserBanner.styles';
559        }
560
561        if ( $this->getTitle() && $this->getTitle()->getNamespace() === NS_FILE ) {
562            $modules['styles']['core'][] = 'filepage'; // local Filepage.css, T31277, T356505
563        }
564
565        return $modules;
566    }
567
568    /**
569     * Preload the existence of three commonly-requested pages in a single query
570     */
571    private function preloadExistence() {
572        $titles = [];
573
574        // User/talk link
575        $user = $this->getUser();
576        if ( $user->isRegistered() ) {
577            $titles[] = $user->getUserPage();
578            $titles[] = $user->getTalkPage();
579        }
580
581        // Check, if the page can hold some kind of content, otherwise do nothing
582        $title = $this->getRelevantTitle();
583        if ( $title && $title->canExist() && $title->canHaveTalkPage() ) {
584            $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
585            if ( $title->isTalkPage() ) {
586                $titles[] = $namespaceInfo->getSubjectPage( $title );
587            } else {
588                $titles[] = $namespaceInfo->getTalkPage( $title );
589            }
590        }
591
592        // Preload for self::getCategoryLinks
593        $allCats = $this->getOutput()->getCategoryLinks();
594        if ( isset( $allCats['normal'] ) && $allCats['normal'] !== [] ) {
595            $catLink = Title::newFromText( $this->msg( 'pagecategorieslink' )->inContentLanguage()->text() );
596            if ( $catLink ) {
597                // If this is a special page, the LinkBatch would skip it
598                $titles[] = $catLink;
599            }
600        }
601
602        $this->getHookRunner()->onSkinPreloadExistence( $titles, $this );
603
604        if ( $titles ) {
605            $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
606            $lb = $linkBatchFactory->newLinkBatch( $titles );
607            $lb->setCaller( __METHOD__ );
608            $lb->execute();
609        }
610    }
611
612    /**
613     * @see self::getRelevantTitle()
614     * @param Title $t
615     */
616    public function setRelevantTitle( $t ) {
617        $this->mRelevantTitle = $t;
618    }
619
620    /**
621     * Return the "relevant" title.
622     * A "relevant" title is not necessarily the actual title of the page.
623     * Special pages like Special:MovePage use set the page they are acting on
624     * as their "relevant" title, this allows the skin system to display things
625     * such as content tabs which belong to that page instead of displaying
626     * a basic special page tab which has almost no meaning.
627     *
628     * @return Title|null the title is null when no relevant title was set, as this
629     *   falls back to ContextSource::getTitle
630     */
631    public function getRelevantTitle() {
632        return $this->mRelevantTitle ?? $this->getTitle();
633    }
634
635    /**
636     * @see self::getRelevantUser()
637     * @param UserIdentity|null $u
638     */
639    public function setRelevantUser( ?UserIdentity $u ) {
640        $this->mRelevantUser = $u;
641    }
642
643    /**
644     * Return the "relevant" user.
645     * A "relevant" user is similar to a relevant title. Special pages like
646     * Special:Contributions mark the user which they are relevant to so that
647     * things like the toolbox can display the information they usually are only
648     * able to display on a user's userpage and talkpage.
649     *
650     * @return UserIdentity|null Null if there's no relevant user or the viewer cannot view it.
651     */
652    public function getRelevantUser(): ?UserIdentity {
653        if ( $this->mRelevantUser === false ) {
654            $this->mRelevantUser = null; // false indicates we never attempted to load it.
655            $title = $this->getRelevantTitle();
656            if ( $title->hasSubjectNamespace( NS_USER ) ) {
657                $services = MediaWikiServices::getInstance();
658                $rootUser = $title->getRootText();
659                $userNameUtils = $services->getUserNameUtils();
660                if ( $userNameUtils->isIP( $rootUser ) ) {
661                    $this->mRelevantUser = UserIdentityValue::newAnonymous( $rootUser );
662                } else {
663                    $user = $services->getUserIdentityLookup()->getUserIdentityByName( $rootUser );
664                    $this->mRelevantUser = $user && $user->isRegistered() ? $user : null;
665                }
666            }
667        }
668
669        // The relevant user should only be set if it exists. However, if it exists but is hidden,
670        // and the viewer cannot see hidden users, this exposes the fact that the user exists;
671        // pretend like the user does not exist in such cases, by setting it to null. T120883
672        if ( $this->mRelevantUser && $this->mRelevantUser->isRegistered() ) {
673            $userBlock = MediaWikiServices::getInstance()
674                ->getBlockManager()
675                ->getBlock( $this->mRelevantUser, null );
676            if ( $userBlock && $userBlock->getHideName() &&
677                !$this->getAuthority()->isAllowed( 'hideuser' )
678            ) {
679                $this->mRelevantUser = null;
680            }
681        }
682
683        return $this->mRelevantUser;
684    }
685
686    /**
687     * Outputs the HTML for the page.
688     * @internal Only to be called by OutputPage.
689     */
690    final public function outputPageFinal( OutputPage $out ) {
691        // generate body
692        ob_start();
693        $this->outputPage();
694        $html = ob_get_contents();
695        ob_end_clean();
696
697        // T259955: OutputPage::headElement must be called last
698        // as it calls OutputPage::getRlClient, which freezes the ResourceLoader
699        // modules queue for the current page load.
700        // Since Skins can add ResourceLoader modules via OutputPage::addModule
701        // and OutputPage::addModuleStyles changing this order can lead to
702        // bugs.
703        $head = $out->headElement( $this );
704        $tail = $out->tailElement( $this );
705
706        echo $head . $html . $tail;
707    }
708
709    /**
710     * Outputs the HTML generated by other functions.
711     */
712    abstract public function outputPage();
713
714    /**
715     * TODO: document
716     * @param Title $title
717     * @return string
718     */
719    public function getPageClasses( $title ) {
720        $services = MediaWikiServices::getInstance();
721        $ns = $title->getNamespace();
722        $numeric = 'ns-' . $ns;
723
724        if ( $title->isSpecialPage() ) {
725            $type = 'ns-special';
726            // T25315: provide a class based on the canonical special page name without subpages
727            [ $canonicalName ] = $services->getSpecialPageFactory()->resolveAlias( $title->getDBkey() );
728            if ( $canonicalName ) {
729                $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
730            } else {
731                $type .= ' mw-invalidspecialpage';
732            }
733        } else {
734            if ( $title->isTalkPage() ) {
735                $type = 'ns-talk';
736            } else {
737                $type = 'ns-subject';
738            }
739            // T208315: add HTML class when the user can edit the page
740            if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
741                $type .= ' mw-editable';
742            }
743        }
744
745        $titleFormatter = $services->getTitleFormatter();
746        $name = Sanitizer::escapeClass( 'page-' . $titleFormatter->getPrefixedText( $title ) );
747        $root = Sanitizer::escapeClass( 'rootpage-' . $titleFormatter->formatTitle( $ns, $title->getRootText() ) );
748        // Add a static class that is not subject to translation to allow extensions/skins/global code to target main
749        // pages reliably (T363281)
750        if ( $title->isMainPage() ) {
751            $name .= ' page-Main_Page';
752        }
753
754        return "$numeric $type $name $root";
755    }
756
757    /**
758     * Return values for <html> element
759     * @return array Array of associative name-to-value elements for <html> element
760     */
761    public function getHtmlElementAttributes() {
762        $lang = $this->getLanguage();
763        return [
764            'lang' => $lang->getHtmlCode(),
765            'dir' => $lang->getDir(),
766            'class' => 'client-nojs',
767        ];
768    }
769
770    /**
771     * @return string HTML
772     */
773    public function getCategoryLinks() {
774        $out = $this->getOutput();
775        $allCats = $out->getCategoryLinks();
776        $title = $this->getTitle();
777        $services = MediaWikiServices::getInstance();
778        $linkRenderer = $services->getLinkRenderer();
779
780        if ( $allCats === [] ) {
781            return '';
782        }
783
784        $embed = "<li>";
785        $pop = "</li>";
786
787        $s = '';
788        $colon = $this->msg( 'colon-separator' )->escaped();
789
790        if ( !empty( $allCats['normal'] ) ) {
791            $t = $embed . implode( $pop . $embed, $allCats['normal'] ) . $pop;
792
793            $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) );
794            $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
795            $pageCategoriesLinkTitle = Title::newFromText( $linkPage );
796            if ( $pageCategoriesLinkTitle ) {
797                $link = $linkRenderer->makeLink( $pageCategoriesLinkTitle, $msg->text() );
798            } else {
799                $link = $msg->escaped();
800            }
801            $s .= Html::rawElement(
802                'div',
803                [ 'id' => 'mw-normal-catlinks', 'class' => 'mw-normal-catlinks' ],
804                $link . $colon . Html::rawElement( 'ul', [], $t )
805            );
806        }
807
808        # Hidden categories
809        if ( isset( $allCats['hidden'] ) ) {
810            $userOptionsLookup = $services->getUserOptionsLookup();
811
812            if ( $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' ) ) {
813                $class = ' mw-hidden-cats-user-shown';
814            } elseif ( $title->inNamespace( NS_CATEGORY ) ) {
815                $class = ' mw-hidden-cats-ns-shown';
816            } else {
817                $class = ' mw-hidden-cats-hidden';
818            }
819
820            $s .= Html::rawElement(
821                'div',
822                [ 'id' => 'mw-hidden-catlinks', 'class' => "mw-hidden-catlinks$class" ],
823                $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
824                    $colon .
825                    Html::rawElement(
826                        'ul',
827                        [],
828                        $embed . implode( $pop . $embed, $allCats['hidden'] ) . $pop
829                    )
830            );
831        }
832
833        return $s;
834    }
835
836    /**
837     * @return string HTML
838     */
839    public function getCategories() {
840        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
841        $showHiddenCats = $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' );
842
843        $catlinks = $this->getCategoryLinks();
844        // Check what we're showing
845        $allCats = $this->getOutput()->getCategoryLinks();
846        $showHidden = $showHiddenCats || $this->getTitle()->inNamespace( NS_CATEGORY );
847
848        $classes = [ 'catlinks' ];
849        if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
850            $classes[] = 'catlinks-allhidden';
851        }
852
853        return Html::rawElement(
854            'div',
855            [ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
856            $catlinks
857        );
858    }
859
860    /**
861     * This runs a hook to allow extensions placing their stuff after content
862     * and article metadata (e.g. categories).
863     * Note: This function has nothing to do with afterContent().
864     *
865     * This hook is placed here in order to allow using the same hook for all
866     * skins, both the SkinTemplate based ones and the older ones, which directly
867     * use this class to get their data.
868     *
869     * The output of this function gets processed in SkinTemplate::outputPage() for
870     * the SkinTemplate based skins, all other skins should directly echo it.
871     *
872     * @return string Empty by default, if not changed by any hook function.
873     */
874    protected function afterContentHook() {
875        $data = '';
876
877        if ( $this->getHookRunner()->onSkinAfterContent( $data, $this ) ) {
878            // adding just some spaces shouldn't toggle the output
879            // of the whole <div/>, so we use trim() here
880            if ( trim( $data ) != '' ) {
881                // Doing this here instead of in the skins to
882                // ensure that the div has the same ID in all
883                // skins
884                $data = "<div id='mw-data-after-content'>\n" .
885                    "\t$data\n" .
886                    "</div>\n";
887            }
888        } else {
889            wfDebug( "Hook SkinAfterContent changed output processing." );
890        }
891
892        return $data;
893    }
894
895    /**
896     * Get the canonical URL (permalink) for the page including oldid if present.
897     *
898     * @return string
899     */
900    private function getCanonicalUrl() {
901        $title = $this->getTitle();
902        $oldid = $this->getOutput()->getRevisionId();
903        if ( $oldid ) {
904            return $title->getCanonicalURL( 'oldid=' . $oldid );
905        } else {
906            // oldid not available for non existing pages
907            return $title->getCanonicalURL();
908        }
909    }
910
911    /**
912     * Text with the permalink to the source page,
913     * usually shown on the footer of a printed page
914     *
915     * @stable to override
916     * @return string HTML text with an URL
917     */
918    public function printSource() {
919        $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
920        $url = htmlspecialchars( $urlUtils->expandIRI( $this->getCanonicalUrl() ) ?? '' );
921
922        return $this->msg( 'retrievedfrom' )
923            ->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
924            ->parse();
925    }
926
927    /**
928     * @return string HTML
929     */
930    public function getUndeleteLink() {
931        $action = $this->getRequest()->getRawVal( 'action' ) ?? 'view';
932        $title = $this->getTitle();
933        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
934
935        if ( ( !$title->exists() || $action == 'history' ) &&
936            $this->getAuthority()->probablyCan( 'deletedhistory', $title )
937        ) {
938            $n = $title->getDeletedEditsCount();
939
940            if ( $n ) {
941                if ( $this->getAuthority()->probablyCan( 'undelete', $title ) ) {
942                    $msg = 'thisisdeleted';
943                } else {
944                    $msg = 'viewdeleted';
945                }
946
947                $subtitle = $this->msg( $msg )->rawParams(
948                    $linkRenderer->makeKnownLink(
949                        SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ),
950                        $this->msg( 'restorelink' )->numParams( $n )->text() )
951                    )->escaped();
952
953                $links = [];
954                // Add link to page logs, unless we're on the history page (which
955                // already has one)
956                if ( $action !== 'history' ) {
957                    $links[] = $linkRenderer->makeKnownLink(
958                        SpecialPage::getTitleFor( 'Log' ),
959                        $this->msg( 'viewpagelogs-lowercase' )->text(),
960                        [],
961                        [ 'page' => $title->getPrefixedText() ]
962                    );
963                }
964
965                // Allow extensions to add more links
966                $this->getHookRunner()->onUndeletePageToolLinks(
967                    $this->getContext(), $linkRenderer, $links );
968
969                if ( $links ) {
970                    $subtitle .= ''
971                        . $this->msg( 'word-separator' )->escaped()
972                        . $this->msg( 'parentheses' )
973                            ->rawParams( $this->getLanguage()->pipeList( $links ) )
974                            ->escaped();
975                }
976
977                return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
978            }
979        }
980
981        return '';
982    }
983
984    /**
985     * @return string
986     */
987    private function subPageSubtitleInternal() {
988        $services = MediaWikiServices::getInstance();
989        $linkRenderer = $services->getLinkRenderer();
990        $out = $this->getOutput();
991        $title = $out->getTitle();
992        $subpages = '';
993
994        if ( !$this->getHookRunner()->onSkinSubPageSubtitle( $subpages, $this, $out ) ) {
995            return $subpages;
996        }
997
998        $hasSubpages = $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() );
999        if ( !$out->isArticle() || !$hasSubpages ) {
1000            return $subpages;
1001        }
1002
1003        $ptext = $title->getPrefixedText();
1004        if ( strpos( $ptext, '/' ) !== false ) {
1005            $links = explode( '/', $ptext );
1006            array_pop( $links );
1007            $count = 0;
1008            $growingLink = '';
1009            $display = '';
1010            $lang = $this->getLanguage();
1011
1012            foreach ( $links as $link ) {
1013                $growingLink .= $link;
1014                $display .= $link;
1015                $linkObj = Title::newFromText( $growingLink );
1016
1017                if ( $linkObj && $linkObj->isKnown() ) {
1018                    $getlink = $linkRenderer->makeKnownLink( $linkObj, $display );
1019
1020                    $count++;
1021
1022                    if ( $count > 1 ) {
1023                        $subpages .= $this->msg( 'pipe-separator' )->escaped();
1024                    } else {
1025                        $subpages .= '&lt; ';
1026                    }
1027
1028                    $subpages .= Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ], $getlink );
1029                    $display = '';
1030                } else {
1031                    $display .= '/';
1032                }
1033                $growingLink .= '/';
1034            }
1035        }
1036
1037        return $subpages;
1038    }
1039
1040    /**
1041     * Helper function for mapping template data for use in legacy function
1042     *
1043     * @param string $dataKey
1044     * @param string $name
1045     * @return string
1046     */
1047    private function getFooterTemplateDataItem( string $dataKey, string $name ) {
1048        $footerData = $this->getComponent( 'footer' )->getTemplateData();
1049        $items = $footerData[ $dataKey ]['array-items'] ?? [];
1050        foreach ( $items as $item ) {
1051            if ( $item['name'] === $name ) {
1052                return $item['html'];
1053            }
1054        }
1055        return '';
1056    }
1057
1058    /**
1059     * @return string
1060     */
1061    final public function getCopyright(): string {
1062        return $this->getFooterTemplateDataItem( 'data-info', 'copyright' );
1063    }
1064
1065    /**
1066     * @param string $align
1067     * @return string
1068     */
1069    public function logoText( $align = '' ) {
1070        if ( $align != '' ) {
1071            $a = " style='float: {$align};'";
1072        } else {
1073            $a = '';
1074        }
1075
1076        $mp = $this->msg( 'mainpage' )->escaped();
1077        $url = htmlspecialchars( Title::newMainPage()->getLocalURL() );
1078
1079        $logourl = RL\SkinModule::getAvailableLogos(
1080            $this->getConfig(),
1081            $this->getLanguage()->getCode()
1082        )[ '1x' ];
1083        return "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
1084    }
1085
1086    /**
1087     * Get template representation of the footer.
1088     *
1089     * Stable to use since 1.40 but should not be overridden.
1090     *
1091     * @since 1.35
1092     * @internal for use inside SkinComponentRegistryContext
1093     * @return array
1094     */
1095    public function getFooterIcons() {
1096        MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'getFooterIcons', '1.40' );
1097        return SkinComponentFooter::getFooterIconsData(
1098            $this->getConfig()
1099        );
1100    }
1101
1102    /**
1103     * Renders a $wgFooterIcons icon according to the method's arguments
1104     *
1105     * Stable to use since 1.40 but should not be overridden.
1106     *
1107     * @param array $icon The icon to build the html for, see $wgFooterIcons
1108     *   for the format of this array.
1109     * @param bool|string $withImage Whether to use the icon's image or output
1110     *   a text-only footericon.
1111     * @return string HTML
1112     */
1113    public function makeFooterIcon( $icon, $withImage = 'withImage' ) {
1114        MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'makeFooterIcon', '1.40' );
1115        return SkinComponentFooter::makeFooterIconHTML(
1116            $this->getConfig(), $icon, $withImage
1117        );
1118    }
1119
1120    /**
1121     * Return URL options for the 'edit page' link.
1122     * This may include an 'oldid' specifier, if the current page view is such.
1123     *
1124     * @return array
1125     * @internal
1126     */
1127    public function editUrlOptions() {
1128        $options = [ 'action' => 'edit' ];
1129        $out = $this->getOutput();
1130
1131        if ( !$out->isRevisionCurrent() ) {
1132            $options['oldid'] = intval( $out->getRevisionId() );
1133        }
1134
1135        return $options;
1136    }
1137
1138    /**
1139     * @param UserIdentity|int $id
1140     * @return bool
1141     */
1142    public function showEmailUser( $id ) {
1143        if ( $id instanceof UserIdentity ) {
1144            $targetUser = User::newFromIdentity( $id );
1145        } else {
1146            $targetUser = User::newFromId( $id );
1147        }
1148
1149        # The sending user must have a confirmed email address and the receiving
1150        # user must accept emails from the sender.
1151        $emailUser = MediaWikiServices::getInstance()->getEmailUserFactory()
1152            ->newEmailUser( $this->getUser() );
1153
1154        return $emailUser->canSend()->isOK()
1155            && $emailUser->validateTarget( $targetUser )->isOK();
1156    }
1157
1158    /* these are used extensively in SkinTemplate, but also some other places */
1159
1160    /**
1161     * @param string|array $urlaction
1162     * @return string
1163     */
1164    public static function makeMainPageUrl( $urlaction = '' ) {
1165        $title = Title::newMainPage();
1166
1167        return $title->getLinkURL( $urlaction );
1168    }
1169
1170    /**
1171     * Make a URL for a Special Page using the given query and protocol.
1172     *
1173     * If $proto is set to null, make a local URL. Otherwise, make a full
1174     * URL with the protocol specified.
1175     *
1176     * @deprecated since 1.39 - Moved to SkinComponentUtils::makeSpecialUrl
1177     * @param string $name Name of the Special page
1178     * @param string|array $urlaction Query to append
1179     * @param string|null $proto Protocol to use or null for a local URL
1180     * @return string
1181     */
1182    public static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1183        wfDeprecated( __METHOD__, '1.39' );
1184        return SkinComponentUtils::makeSpecialUrl( $name, $urlaction, $proto );
1185    }
1186
1187    /**
1188     * @deprecated since 1.39 - Moved to SkinComponentUtils::makeSpecialUrlSubpage
1189     * @param string $name
1190     * @param string|bool $subpage false for no subpage
1191     * @param string|array $urlaction
1192     * @return string
1193     */
1194    public static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1195        wfDeprecated( __METHOD__, '1.39' );
1196        return SkinComponentUtils::makeSpecialUrlSubpage( $name, $subpage, $urlaction );
1197    }
1198
1199    /**
1200     * If url string starts with http, consider as external URL, else
1201     * internal
1202     * @param string $name
1203     * @return string URL
1204     */
1205    public static function makeInternalOrExternalUrl( $name ) {
1206        $protocols = MediaWikiServices::getInstance()->getUrlUtils()->validProtocols();
1207
1208        if ( preg_match( '/^(?i:' . $protocols . ')/', $name ) ) {
1209            return $name;
1210        } else {
1211            $title = $name instanceof Title ? $name : Title::newFromText( $name );
1212            return $title ? $title->getLocalURL() : '';
1213        }
1214    }
1215
1216    /**
1217     * these return an array with the 'href' and boolean 'exists'
1218     * @param string|Title $name
1219     * @param string|array $urlaction
1220     * @return array
1221     */
1222    protected static function makeUrlDetails( $name, $urlaction = '' ) {
1223        $title = $name instanceof Title ? $name : Title::newFromText( $name );
1224        return [
1225            'href' => $title ? $title->getLocalURL( $urlaction ) : '',
1226            'exists' => $title && $title->isKnown(),
1227        ];
1228    }
1229
1230    /**
1231     * Make URL details where the article exists (or at least it's convenient to think so)
1232     * @param string|Title $name Article name
1233     * @param string|array $urlaction
1234     * @return array
1235     */
1236    protected static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1237        $title = $name instanceof Title ? $name : Title::newFromText( $name );
1238        return [
1239            'href' => $title ? $title->getLocalURL( $urlaction ) : '',
1240            'exists' => (bool)$title,
1241        ];
1242    }
1243
1244    /**
1245     * Allows correcting the language of interlanguage links which, mostly due to
1246     * legacy reasons, do not always match the standards compliant language tag.
1247     *
1248     * @param string $code
1249     * @return string
1250     * @since 1.35
1251     */
1252    public function mapInterwikiToLanguage( $code ) {
1253        $map = $this->getConfig()->get( MainConfigNames::InterlanguageLinkCodeMap );
1254        return $map[ $code ] ?? $code;
1255    }
1256
1257    /**
1258     * Generates array of language links for the current page.
1259     * This may includes items added to this section by the SidebarBeforeOutput hook
1260     * (which may not necessarily be language links)
1261     *
1262     * @since 1.35
1263     * @return array
1264     */
1265    public function getLanguages() {
1266        if ( $this->getConfig()->get( MainConfigNames::HideInterlanguageLinks ) ) {
1267            return [];
1268        }
1269        if ( $this->languageLinks === null ) {
1270            $hookRunner = $this->getHookRunner();
1271
1272            $userLang = $this->getLanguage();
1273            $languageLinks = [];
1274            $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1275
1276            foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
1277                [ $prefix, $title ] = explode( ':', $languageLinkText, 2 );
1278                $class = 'interlanguage-link interwiki-' . $prefix;
1279
1280                [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
1281                $languageLinkTitle = TitleValue::tryNew( NS_MAIN, $title, $frag, $prefix );
1282                if ( $languageLinkTitle === null ) {
1283                    continue;
1284                }
1285                $ilInterwikiCode = $this->mapInterwikiToLanguage( $prefix );
1286
1287                $ilLangName = $langNameUtils->getLanguageName( $ilInterwikiCode );
1288
1289                if ( strval( $ilLangName ) === '' ) {
1290                    $ilDisplayTextMsg = $this->msg( "interlanguage-link-$ilInterwikiCode" );
1291                    if ( !$ilDisplayTextMsg->isDisabled() ) {
1292                        // Use custom MW message for the display text
1293                        $ilLangName = $ilDisplayTextMsg->text();
1294                    } else {
1295                        // Last resort: fallback to the language link target
1296                        $ilLangName = $languageLinkText;
1297                    }
1298                } else {
1299                    // Use the language autonym as display text
1300                    $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
1301                }
1302
1303                // CLDR extension or similar is required to localize the language name;
1304                // otherwise we'll end up with the autonym again.
1305                $ilLangLocalName =
1306                    $langNameUtils->getLanguageName( $ilInterwikiCode, $userLang->getCode() );
1307
1308                $languageLinkTitleText = $languageLinkTitle->getText();
1309                if ( $ilLangLocalName === '' ) {
1310                    $ilFriendlySiteName =
1311                        $this->msg( "interlanguage-link-sitename-$ilInterwikiCode" );
1312                    if ( !$ilFriendlySiteName->isDisabled() ) {
1313                        if ( $languageLinkTitleText === '' ) {
1314                            $ilTitle =
1315                                $this->msg( 'interlanguage-link-title-nonlangonly',
1316                                    $ilFriendlySiteName->text() )->text();
1317                        } else {
1318                            $ilTitle =
1319                                $this->msg( 'interlanguage-link-title-nonlang',
1320                                    $languageLinkTitleText, $ilFriendlySiteName->text() )->text();
1321                        }
1322                    } else {
1323                        // we have nothing friendly to put in the title, so fall back to
1324                        // displaying the interlanguage link itself in the title text
1325                        // (similar to what is done in page content)
1326                        $ilTitle = $languageLinkTitle->getInterwiki() . ":$languageLinkTitleText";
1327                    }
1328                } elseif ( $languageLinkTitleText === '' ) {
1329                    $ilTitle =
1330                        $this->msg( 'interlanguage-link-title-langonly', $ilLangLocalName )->text();
1331                } else {
1332                    $ilTitle =
1333                        $this->msg( 'interlanguage-link-title', $languageLinkTitleText,
1334                            $ilLangLocalName )->text();
1335                }
1336
1337                $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
1338                // A TitleValue is sufficient above this point, but we need
1339                // a full Title for ::getFullURL() and the hook invocation
1340                $languageLinkFullTitle = Title::newFromLinkTarget( $languageLinkTitle );
1341                $languageLink = [
1342                    'href' => $languageLinkFullTitle->getFullURL(),
1343                    'text' => $ilLangName,
1344                    'title' => $ilTitle,
1345                    'class' => $class,
1346                    'link-class' => 'interlanguage-link-target',
1347                    'lang' => $ilInterwikiCodeBCP47,
1348                    'hreflang' => $ilInterwikiCodeBCP47,
1349                    'data-title' => $languageLinkTitleText,
1350                    'data-language-autonym' => $ilLangName,
1351                    'data-language-local-name' => $ilLangLocalName,
1352                ];
1353                $hookRunner->onSkinTemplateGetLanguageLink(
1354                    $languageLink, $languageLinkFullTitle, $this->getTitle(), $this->getOutput()
1355                );
1356                $languageLinks[] = $languageLink;
1357            }
1358            $this->languageLinks = $languageLinks;
1359        }
1360
1361        return $this->languageLinks;
1362    }
1363
1364    /**
1365     * Build array of common navigation links.
1366     * Assumes thispage property has been set before execution.
1367     * @since 1.35
1368     * @return array
1369     */
1370    protected function buildNavUrls() {
1371        $out = $this->getOutput();
1372        $title = $this->getTitle();
1373        $thispage = $title->getPrefixedDBkey();
1374        $uploadNavigationUrl = $this->getConfig()->get( MainConfigNames::UploadNavigationUrl );
1375
1376        $nav_urls = [];
1377        $nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1378        if ( $uploadNavigationUrl ) {
1379            $nav_urls['upload'] = [ 'href' => $uploadNavigationUrl ];
1380        } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getAuthority() ) === true ) {
1381            $nav_urls['upload'] = [ 'href' => SkinComponentUtils::makeSpecialUrl( 'Upload' ) ];
1382        } else {
1383            $nav_urls['upload'] = false;
1384        }
1385        $nav_urls['specialpages'] = [ 'href' => SkinComponentUtils::makeSpecialUrl( 'Specialpages' ) ];
1386
1387        $nav_urls['print'] = false;
1388        $nav_urls['permalink'] = false;
1389        $nav_urls['info'] = false;
1390        $nav_urls['whatlinkshere'] = false;
1391        $nav_urls['recentchangeslinked'] = false;
1392        $nav_urls['contributions'] = false;
1393        $nav_urls['log'] = false;
1394        $nav_urls['blockip'] = false;
1395        $nav_urls['changeblockip'] = false;
1396        $nav_urls['unblockip'] = false;
1397        $nav_urls['mute'] = false;
1398        $nav_urls['emailuser'] = false;
1399        $nav_urls['userrights'] = false;
1400
1401        // A print stylesheet is attached to all pages, but nobody ever
1402        // figures that out. :)  Add a link...
1403        if ( !$out->isPrintable() && ( $out->isArticle() || $title->isSpecialPage() ) ) {
1404            $nav_urls['print'] = [
1405                'text' => $this->msg( 'printableversion' )->text(),
1406                'href' => 'javascript:print();'
1407            ];
1408        }
1409
1410        if ( $out->isArticle() ) {
1411            // Also add a "permalink" while we're at it
1412            $revid = $out->getRevisionId();
1413            if ( $revid ) {
1414                $nav_urls['permalink'] = [
1415                    'icon' => 'link',
1416                    'text' => $this->msg( 'permalink' )->text(),
1417                    'href' => $title->getLocalURL( "oldid=$revid" )
1418                ];
1419            }
1420        }
1421
1422        if ( $out->isArticleRelated() ) {
1423            $nav_urls['whatlinkshere'] = [
1424                'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $thispage )->getLocalURL()
1425            ];
1426
1427            $nav_urls['info'] = [
1428                'icon' => 'infoFilled',
1429                'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1430                'href' => $title->getLocalURL( "action=info" )
1431            ];
1432
1433            if ( $title->exists() || $title->inNamespace( NS_CATEGORY ) ) {
1434                $nav_urls['recentchangeslinked'] = [
1435                    'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $thispage )->getLocalURL()
1436                ];
1437            }
1438        }
1439
1440        $user = $this->getRelevantUser();
1441
1442        if ( $user ) {
1443            $rootUser = $user->getName();
1444
1445            $nav_urls['contributions'] = [
1446                'text' => $this->msg( 'tool-link-contributions', $rootUser )->text(),
1447                'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1448                'tooltip-params' => [ $rootUser ],
1449            ];
1450
1451            $nav_urls['log'] = [
1452                'icon' => 'listBullet',
1453                'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Log', $rootUser )
1454            ];
1455
1456            if ( $this->getAuthority()->isAllowed( 'block' ) ) {
1457                // Check if the user is already blocked
1458                $userBlock = MediaWikiServices::getInstance()
1459                ->getBlockManager()
1460                ->getBlock( $user, null );
1461                if ( $userBlock ) {
1462                    $nav_urls['changeblockip'] = [
1463                        'icon' => 'block',
1464                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Block', $rootUser )
1465                    ];
1466                    $nav_urls['unblockip'] = [
1467                        'icon' => 'unBlock',
1468                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Unblock', $rootUser )
1469                    ];
1470                } else {
1471                    $nav_urls['blockip'] = [
1472                        'icon' => 'block',
1473                        'text' => $this->msg( 'blockip', $rootUser )->text(),
1474                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Block', $rootUser )
1475                    ];
1476                }
1477            }
1478
1479            if ( $this->showEmailUser( $user ) ) {
1480                $nav_urls['emailuser'] = [
1481                    'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1482                    'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1483                    'tooltip-params' => [ $rootUser ],
1484                ];
1485            }
1486
1487            if ( $user->isRegistered() ) {
1488                if ( $this->getConfig()->get( MainConfigNames::EnableSpecialMute ) &&
1489                    $this->getUser()->isNamed()
1490                ) {
1491                    $nav_urls['mute'] = [
1492                        'text' => $this->msg( 'mute-preferences' )->text(),
1493                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Mute', $rootUser )
1494                    ];
1495                }
1496
1497                // Don't show links to Special:UserRights for temporary accounts (as they cannot have groups)
1498                $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
1499                if ( !$userNameUtils->isTemp( $user->getName() ) ) {
1500                    $sur = new SpecialUserRights;
1501                    $sur->setContext( $this->getContext() );
1502                    $canChange = $sur->userCanChangeRights( $user );
1503                    $delimiter = $this->getConfig()->get(
1504                        MainConfigNames::UserrightsInterwikiDelimiter );
1505                    if ( str_contains( $rootUser, $delimiter ) ) {
1506                        // Username contains interwiki delimiter, link it via the
1507                        // #{userid} syntax. (T260222)
1508                        $linkArgs = [ false, [ 'user' => '#' . $user->getId() ] ];
1509                    } else {
1510                        $linkArgs = [ $rootUser ];
1511                    }
1512                    $nav_urls['userrights'] = [
1513                        'icon' => 'userGroup',
1514                        'text' => $this->msg(
1515                            $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly',
1516                            $rootUser
1517                        )->text(),
1518                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Userrights', ...$linkArgs )
1519                    ];
1520                }
1521            }
1522        }
1523
1524        return $nav_urls;
1525    }
1526
1527    /**
1528     * Build data structure representing syndication links.
1529     * @since 1.35
1530     * @return array
1531     */
1532    final protected function buildFeedUrls() {
1533        $feeds = [];
1534        $out = $this->getOutput();
1535        if ( $out->isSyndicated() ) {
1536            foreach ( $out->getSyndicationLinks() as $format => $link ) {
1537                $feeds[$format] = [
1538                    // Messages: feed-atom, feed-rss
1539                    'text' => $this->msg( "feed-$format" )->text(),
1540                    'href' => $link
1541                ];
1542            }
1543        }
1544        return $feeds;
1545    }
1546
1547    /**
1548     * Build an array that represents the sidebar(s), the navigation bar among them.
1549     *
1550     * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
1551     *
1552     * The format of the returned array is [ heading => content, ... ], where:
1553     * - heading is the heading of a navigation portlet. It is either:
1554     *   - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
1555     *     (Note that 'SEARCH' unlike others is not supported out-of-the-box by the skins.
1556     *     For it to work, a skin must add custom support for it.)
1557     *   - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
1558     *   - plain text, which should be HTML-escaped by the skin
1559     * - content is the contents of the portlet.
1560     *   - For keys that aren't magic strings, this is an array of link data, where the
1561     *     array items are arrays in the format expected by the $item parameter of
1562     *     {@link self::makeListItem()}.
1563     *   - For magic strings, the format varies. For LANGUAGES and TOOLBOX it is the same as above;
1564     *     for SEARCH the value will be ignored.
1565     *
1566     * Note that extensions can control the sidebar contents using the SkinBuildSidebar hook
1567     * and can technically insert anything in here; skin creators are expected to handle
1568     * values described above.
1569     *
1570     * @return array
1571     */
1572    public function buildSidebar() {
1573        if ( $this->sidebar === null ) {
1574            $services = MediaWikiServices::getInstance();
1575            $callback = function ( $old = null, &$ttl = null ) {
1576                $bar = [];
1577                $this->addToSidebar( $bar, 'sidebar' );
1578
1579                // This hook may vary its behaviour by skin.
1580                $this->getHookRunner()->onSkinBuildSidebar( $this, $bar );
1581                $msgCache = MediaWikiServices::getInstance()->getMessageCache();
1582                if ( $msgCache->isDisabled() ) {
1583                    // Don't cache the fallback if DB query failed. T133069
1584                    $ttl = WANObjectCache::TTL_UNCACHEABLE;
1585                }
1586
1587                return $bar;
1588            };
1589
1590            $msgCache = $services->getMessageCache();
1591            $wanCache = $services->getMainWANObjectCache();
1592            $config = $this->getConfig();
1593            $languageCode = $this->getLanguage()->getCode();
1594
1595            $sidebar = $config->get( MainConfigNames::EnableSidebarCache )
1596                ? $wanCache->getWithSetCallback(
1597                    $wanCache->makeKey( 'sidebar', $languageCode, $this->getSkinName() ?? '' ),
1598                    $config->get( MainConfigNames::SidebarCacheExpiry ),
1599                    $callback,
1600                    [
1601                        'checkKeys' => [
1602                            // Unless there is both no exact $code override nor an i18n definition
1603                            // in the software, the only MediaWiki page to check is for $code.
1604                            $msgCache->getCheckKey( $languageCode )
1605                        ],
1606                        'lockTSE' => 30
1607                    ]
1608                )
1609                : $callback();
1610
1611            $sidebar['TOOLBOX'] = array_merge(
1612                $this->makeToolbox(
1613                    $this->buildNavUrls(),
1614                    $this->buildFeedUrls()
1615                ), $sidebar['TOOLBOX'] ?? []
1616            );
1617
1618            $sidebar['LANGUAGES'] = $this->getLanguages();
1619            // Apply post-processing to the cached value
1620            $this->getHookRunner()->onSidebarBeforeOutput( $this, $sidebar );
1621            $this->sidebar = $sidebar;
1622        }
1623
1624        return $this->sidebar;
1625    }
1626
1627    /**
1628     * Add content from a sidebar system message
1629     * Currently only used for MediaWiki:Sidebar (but may be used by Extensions)
1630     *
1631     * This is just a wrapper around addToSidebarPlain() for backwards compatibility
1632     *
1633     * @param array &$bar
1634     * @param string $message
1635     */
1636    public function addToSidebar( &$bar, $message ) {
1637        $this->addToSidebarPlain( $bar, $this->msg( $message )->inContentLanguage()->plain() );
1638    }
1639
1640    /**
1641     * Add content from plain text
1642     * @since 1.17
1643     * @param array &$bar
1644     * @param string $text
1645     * @return array
1646     */
1647    public function addToSidebarPlain( &$bar, $text ) {
1648        $lines = explode( "\n", $text );
1649
1650        $heading = '';
1651        $config = $this->getConfig();
1652        $messageTitle = $config->get( MainConfigNames::EnableSidebarCache )
1653            ? Title::newMainPage() : $this->getTitle();
1654        $services = MediaWikiServices::getInstance();
1655        $messageCache = $services->getMessageCache();
1656        $urlUtils = $services->getUrlUtils();
1657
1658        foreach ( $lines as $line ) {
1659            if ( strpos( $line, '*' ) !== 0 ) {
1660                continue;
1661            }
1662            $line = rtrim( $line, "\r" ); // for Windows compat
1663
1664            if ( strpos( $line, '**' ) !== 0 ) {
1665                $heading = trim( $line, '* ' );
1666                if ( !array_key_exists( $heading, $bar ) ) {
1667                    $bar[$heading] = [];
1668                }
1669            } else {
1670                $line = trim( $line, '* ' );
1671
1672                if ( strpos( $line, '|' ) !== false ) {
1673                    $line = $messageCache->transform( $line, false, null, $messageTitle );
1674                    $line = array_map( 'trim', explode( '|', $line, 2 ) );
1675                    if ( count( $line ) !== 2 ) {
1676                        // Second check, could be hit by people doing
1677                        // funky stuff with parserfuncs... (T35321)
1678                        continue;
1679                    }
1680
1681                    $extraAttribs = [];
1682
1683                    $msgLink = $this->msg( $line[0] )->page( $messageTitle )->inContentLanguage();
1684                    if ( $msgLink->exists() ) {
1685                        $link = $msgLink->text();
1686                        if ( $link == '-' ) {
1687                            continue;
1688                        }
1689                    } else {
1690                        $link = $line[0];
1691                    }
1692                    $msgText = $this->msg( $line[1] )->page( $messageTitle );
1693                    if ( $msgText->exists() ) {
1694                        $text = $msgText->text();
1695                    } else {
1696                        $text = $line[1];
1697                    }
1698
1699                    if ( preg_match( '/^(?i:' . $urlUtils->validProtocols() . ')/', $link ) ) {
1700                        $href = $link;
1701
1702                        // Parser::getExternalLinkAttribs won't work here because of the Namespace things
1703                        if ( $config->get( MainConfigNames::NoFollowLinks ) &&
1704                            !$urlUtils->matchesDomainList(
1705                                (string)$href,
1706                                (array)$config->get( MainConfigNames::NoFollowDomainExceptions )
1707                            )
1708                        ) {
1709                            $extraAttribs['rel'] = 'nofollow';
1710                        }
1711
1712                        if ( $config->get( MainConfigNames::ExternalLinkTarget ) ) {
1713                            $extraAttribs['target'] =
1714                                $config->get( MainConfigNames::ExternalLinkTarget );
1715                        }
1716                    } else {
1717                        $title = Title::newFromText( $link );
1718                        $href = $title ? $title->fixSpecialName()->getLinkURL() : '';
1719                    }
1720
1721                    $id = strtr( $line[1], ' ', '-' );
1722                    $bar[$heading][] = array_merge( [
1723                        'text' => $text,
1724                        'href' => $href,
1725                        'icon' => $this->getSidebarIcon( $id ),
1726                        'id' => Sanitizer::escapeIdForAttribute( 'n-' . $id ),
1727                        'active' => false,
1728                    ], $extraAttribs );
1729                }
1730            }
1731        }
1732
1733        return $bar;
1734    }
1735
1736    /**
1737     * @param string $id the id of the menu
1738     * @return string|null the icon glyph name to associate with this menu
1739     */
1740    private function getSidebarIcon( string $id ) {
1741        switch ( $id ) {
1742            case 'mainpage-description':
1743                return 'home';
1744            case 'randompage':
1745                return 'die';
1746            case 'recentchanges':
1747                return 'recentChanges';
1748            // These menu items are commonly added in MediaWiki:Sidebar. We should
1749            // reconsider the location of this logic in future.
1750            case 'help':
1751            case 'help-mediawiki':
1752                return 'help';
1753            default:
1754                return null;
1755        }
1756    }
1757
1758    /**
1759     * Check whether to allow new talk page notifications for the current request.
1760     *
1761     * The client might be reading without a session cookie from an IP that matches
1762     * a previous IP editor. When clients without a session visit a page with a CDN miss,
1763     * we must not embed personal notifications, as doing so might leak personal details
1764     * (if Cache-Control is public), or risk an outage per T350861 (if max-age=0).
1765     *
1766     * From an end-user perspective, this has the added benefit of not randomly showing
1767     * notifications to readers (on page views that happen to be a CDN miss) when
1768     * sharing an IP with an editor. Notifying clients without a session is not reliably
1769     * possible, as their requests are usually CDN hits.
1770     *
1771     * @see https://phabricator.wikimedia.org/T350861
1772     * @return bool
1773     */
1774    private function hideNewTalkMessagesForCurrentSession() {
1775        // Only show new talk page notification if there is a session,
1776        // (the client edited a page from this browser, or is logged-in).
1777        return !$this->getRequest()->getSession()->isPersistent();
1778    }
1779
1780    /**
1781     * Gets new talk page messages for the current user and returns an
1782     * appropriate alert message (or an empty string if there are no messages)
1783     * @return string
1784     */
1785    public function getNewtalks() {
1786        if ( $this->hideNewTalkMessagesForCurrentSession() ) {
1787            return '';
1788        }
1789
1790        $newMessagesAlert = '';
1791        $user = $this->getUser();
1792        $services = MediaWikiServices::getInstance();
1793        $linkRenderer = $services->getLinkRenderer();
1794        $userHasNewMessages = $services->getTalkPageNotificationManager()
1795            ->userHasNewMessages( $user );
1796        $timestamp = $services->getTalkPageNotificationManager()
1797            ->getLatestSeenMessageTimestamp( $user );
1798        $newtalks = !$userHasNewMessages ? [] : [
1799            [
1800            // TODO: Deprecate adding wiki and link to array and redesign GetNewMessagesAlert hook
1801            'wiki' => WikiMap::getCurrentWikiId(),
1802            'link' => $user->getTalkPage()->getLocalURL(),
1803            'rev' => $timestamp ? $services->getRevisionLookup()
1804                ->getRevisionByTimestamp( $user->getTalkPage(), $timestamp ) : null
1805            ]
1806        ];
1807        $out = $this->getOutput();
1808
1809        // Allow extensions to disable or modify the new messages alert
1810        if ( !$this->getHookRunner()->onGetNewMessagesAlert(
1811            $newMessagesAlert, $newtalks, $user, $out )
1812        ) {
1813            return '';
1814        }
1815        if ( $newMessagesAlert ) {
1816            return $newMessagesAlert;
1817        }
1818
1819        if ( $newtalks !== [] ) {
1820            $uTalkTitle = $user->getTalkPage();
1821            $lastSeenRev = $newtalks[0]['rev'];
1822            $numAuthors = 0;
1823            if ( $lastSeenRev !== null ) {
1824                $plural = true; // Default if we have a last seen revision: if unknown, use plural
1825                $revStore = $services->getRevisionStore();
1826                $latestRev = $revStore->getRevisionByTitle(
1827                    $uTalkTitle,
1828                    0,
1829                    IDBAccessObject::READ_NORMAL
1830                );
1831                if ( $latestRev !== null ) {
1832                    // Singular if only 1 unseen revision, plural if several unseen revisions.
1833                    $plural = $latestRev->getParentId() !== $lastSeenRev->getId();
1834                    $numAuthors = $revStore->countAuthorsBetween(
1835                        $uTalkTitle->getArticleID(),
1836                        $lastSeenRev,
1837                        $latestRev,
1838                        null,
1839                        10,
1840                        RevisionStore::INCLUDE_NEW
1841                    );
1842                }
1843            } else {
1844                // Singular if no revision -> diff link will show latest change only in any case
1845                $plural = false;
1846            }
1847            $plural = $plural ? 999 : 1;
1848            // 999 signifies "more than one revision". We don't know how many, and even if we did,
1849            // the number of revisions or authors is not necessarily the same as the number of
1850            // "messages".
1851            $newMessagesLink = $linkRenderer->makeKnownLink(
1852                $uTalkTitle,
1853                $this->msg( 'new-messages-link-plural' )->params( $plural )->text(),
1854                [],
1855                $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
1856            );
1857
1858            $newMessagesDiffLink = $linkRenderer->makeKnownLink(
1859                $uTalkTitle,
1860                $this->msg( 'new-messages-diff-link-plural' )->params( $plural )->text(),
1861                [],
1862                $lastSeenRev !== null
1863                    ? [ 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ]
1864                    : [ 'diff' => 'cur' ]
1865            );
1866
1867            if ( $numAuthors >= 1 && $numAuthors <= 10 ) {
1868                $newMessagesAlert = $this->msg(
1869                    'new-messages-from-users'
1870                )->rawParams(
1871                    $newMessagesLink,
1872                    $newMessagesDiffLink
1873                )->numParams(
1874                    $numAuthors,
1875                    $plural
1876                );
1877            } else {
1878                // $numAuthors === 11 signifies "11 or more" ("more than 10")
1879                $newMessagesAlert = $this->msg(
1880                    $numAuthors > 10 ? 'new-messages-from-many-users' : 'new-messages'
1881                )->rawParams(
1882                    $newMessagesLink,
1883                    $newMessagesDiffLink
1884                )->numParams( $plural );
1885            }
1886            $newMessagesAlert = $newMessagesAlert->parse();
1887        }
1888
1889        return $newMessagesAlert;
1890    }
1891
1892    /**
1893     * Get a cached notice
1894     *
1895     * @param string $name Message name, or 'default' for $wgSiteNotice
1896     * @return string|false HTML fragment, or false to indicate that the caller
1897     *   should fall back to the next notice in its sequence
1898     */
1899    private function getCachedNotice( $name ) {
1900        $config = $this->getConfig();
1901
1902        if ( $name === 'default' ) {
1903            // special case
1904            $notice = $config->get( MainConfigNames::SiteNotice );
1905            if ( !$notice ) {
1906                return false;
1907            }
1908        } else {
1909            $msg = $this->msg( $name )->inContentLanguage();
1910            if ( $msg->isBlank() ) {
1911                return '';
1912            } elseif ( $msg->isDisabled() ) {
1913                return false;
1914            }
1915            $notice = $msg->plain();
1916        }
1917
1918        $services = MediaWikiServices::getInstance();
1919        $cache = $services->getMainWANObjectCache();
1920        $parsed = $cache->getWithSetCallback(
1921            // Use the extra hash appender to let eg SSL variants separately cache
1922            // Key is verified with md5 hash of unparsed wikitext
1923            $cache->makeKey(
1924                $name, $config->get( MainConfigNames::RenderHashAppend ), md5( $notice ) ),
1925            // TTL in seconds
1926            600,
1927            function () use ( $notice ) {
1928                return $this->getOutput()->parseAsInterface( $notice );
1929            }
1930        );
1931
1932        $contLang = $services->getContentLanguage();
1933        return Html::rawElement(
1934            'div',
1935            [
1936                'class' => $name,
1937                'lang' => $contLang->getHtmlCode(),
1938                'dir' => $contLang->getDir()
1939            ],
1940            $parsed
1941        );
1942    }
1943
1944    /**
1945     * @return string HTML fragment
1946     */
1947    public function getSiteNotice() {
1948        $siteNotice = '';
1949
1950        if ( $this->getHookRunner()->onSiteNoticeBefore( $siteNotice, $this ) ) {
1951            if ( $this->getUser()->isRegistered() ) {
1952                $siteNotice = $this->getCachedNotice( 'sitenotice' );
1953            } else {
1954                $anonNotice = $this->getCachedNotice( 'anonnotice' );
1955                if ( $anonNotice === false ) {
1956                    $siteNotice = $this->getCachedNotice( 'sitenotice' );
1957                } else {
1958                    $siteNotice = $anonNotice;
1959                }
1960            }
1961            if ( $siteNotice === false ) {
1962                $siteNotice = $this->getCachedNotice( 'default' ) ?: '';
1963            }
1964            if ( $this->canUseWikiPage() ) {
1965                $ns = $this->getWikiPage()->getNamespace();
1966                $nsNotice = $this->getCachedNotice( "namespacenotice-$ns" );
1967                if ( $nsNotice ) {
1968                    $siteNotice .= $nsNotice;
1969                }
1970            }
1971            if ( $siteNotice !== '' ) {
1972                $siteNotice = Html::rawElement( 'div', [ 'id' => 'localNotice', 'data-nosnippet' => '' ], $siteNotice );
1973            }
1974        }
1975
1976        $this->getHookRunner()->onSiteNoticeAfter( $siteNotice, $this );
1977        if ( $this->getOptions()[ 'wrapSiteNotice' ] ) {
1978            $siteNotice = Html::rawElement( 'div', [ 'id' => 'siteNotice' ], $siteNotice );
1979        }
1980        return $siteNotice;
1981    }
1982
1983    /**
1984     * Create a section edit link.
1985     *
1986     * @param Title $nt The title being linked to (may not be the same as
1987     *   the current page, if the section is included from a template)
1988     * @param string $section The designation of the section being pointed to,
1989     *   to be included in the link, like "&section=$section"
1990     * @param string $sectionTitle Section title. It is used in the link tooltip, escaped and
1991     *   wrapped in the 'editsectionhint' message
1992     * @param Language $lang
1993     * @return string HTML to use for edit link
1994     */
1995    public function doEditSectionLink( Title $nt, $section, $sectionTitle, Language $lang ) {
1996        // HTML generated here should probably have userlangattributes
1997        // added to it for LTR text on RTL pages
1998
1999        $attribs = [];
2000        $attribs['title'] = $this->msg( 'editsectionhint' )->plaintextParams( $sectionTitle )
2001            ->inLanguage( $lang )->text();
2002
2003        $links = [
2004            'editsection' => [
2005                'icon' => 'edit',
2006                'text' => $this->msg( 'editsection' )->inLanguage( $lang )->text(),
2007                'targetTitle' => $nt,
2008                'attribs' => $attribs,
2009                'query' => [ 'action' => 'edit', 'section' => $section ]
2010            ]
2011        ];
2012
2013        $this->getHookRunner()->onSkinEditSectionLinks( $this, $nt, $section, $sectionTitle, $links, $lang );
2014
2015        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
2016        $newLinks = [];
2017        $options = $this->defaultLinkOptions + [
2018            'class-as-property' => true,
2019        ];
2020        $ctx = $this->getContext();
2021        foreach ( $links as $key => $linkDetails ) {
2022            $targetTitle = $linkDetails['targetTitle'];
2023            $attrs = $linkDetails['attribs'];
2024            $query = $linkDetails['query'];
2025            unset( $linkDetails['targetTitle'] );
2026            unset( $linkDetails['query'] );
2027            unset( $linkDetails['attribs'] );
2028            unset( $linkDetails['options' ] );
2029            $component = new SkinComponentLink(
2030                $key, $linkDetails + [
2031                    'href' => Title::newFromLinkTarget( $targetTitle )->getLinkURL( $query, false ),
2032                ] + $attrs, $ctx, $options
2033            );
2034            $newLinks[] = $component->getTemplateData();
2035        }
2036        return $this->doEditSectionLinksHTML( $newLinks, $lang );
2037    }
2038
2039    /**
2040     * @stable to override by skins
2041     *
2042     * @param array $links
2043     * @param Language $lang
2044     * @return string
2045     */
2046    protected function doEditSectionLinksHTML( array $links, Language $lang ) {
2047        $result = Html::openElement( 'span', [ 'class' => 'mw-editsection' ] );
2048        $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], '[' );
2049
2050        $linksHtml = [];
2051        foreach ( $links as $linkDetails ) {
2052            $linksHtml[] = $linkDetails['html'];
2053        }
2054
2055        $result .= implode(
2056            Html::rawElement(
2057                'span',
2058                [ 'class' => 'mw-editsection-divider' ],
2059                $this->msg( 'pipe-separator' )->inLanguage( $lang )->escaped()
2060            ),
2061            $linksHtml
2062        );
2063
2064        $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], ']' );
2065        $result .= Html::closeElement( 'span' );
2066        return $result;
2067    }
2068
2069    /**
2070     * Create an array of common toolbox items from the data in the quicktemplate
2071     * stored by SkinTemplate.
2072     * The resulting array is built according to a format intended to be passed
2073     * through makeListItem to generate the html.
2074     * @param array $navUrls
2075     * @param array $feedUrls
2076     * @return array
2077     */
2078    public function makeToolbox( $navUrls, $feedUrls ) {
2079        $toolbox = [];
2080        if ( $navUrls['whatlinkshere'] ?? null ) {
2081            $toolbox['whatlinkshere'] = $navUrls['whatlinkshere'];
2082            $toolbox['whatlinkshere']['id'] = 't-whatlinkshere';
2083            $toolbox['whatlinkshere']['icon'] = 'articleRedirect';
2084        }
2085        if ( $navUrls['recentchangeslinked'] ?? null ) {
2086            $toolbox['recentchangeslinked'] = $navUrls['recentchangeslinked'];
2087            $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
2088            $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
2089            $toolbox['recentchangeslinked']['rel'] = 'nofollow';
2090        }
2091        if ( $feedUrls ) {
2092            $toolbox['feeds']['id'] = 'feedlinks';
2093            $toolbox['feeds']['links'] = [];
2094            foreach ( $feedUrls as $key => $feed ) {
2095                $toolbox['feeds']['links'][$key] = $feed;
2096                $toolbox['feeds']['links'][$key]['id'] = "feed-$key";
2097                $toolbox['feeds']['links'][$key]['rel'] = 'alternate';
2098                $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml";
2099                $toolbox['feeds']['links'][$key]['class'] = 'feedlink';
2100            }
2101        }
2102        foreach ( [ 'contributions', 'log', 'blockip', 'changeblockip', 'unblockip',
2103            'emailuser', 'mute', 'userrights', 'upload', 'specialpages' ] as $special
2104        ) {
2105            if ( $navUrls[$special] ?? null ) {
2106                $toolbox[$special] = $navUrls[$special];
2107                $toolbox[$special]['id'] = "t-$special";
2108            }
2109        }
2110        if ( $navUrls['print'] ?? null ) {
2111            $toolbox['print'] = $navUrls['print'];
2112            $toolbox['print']['id'] = 't-print';
2113            $toolbox['print']['rel'] = 'alternate';
2114            $toolbox['print']['msg'] = 'printableversion';
2115        }
2116        if ( $navUrls['permalink'] ?? null ) {
2117            $toolbox['permalink'] = $navUrls['permalink'];
2118            $toolbox['permalink']['id'] = 't-permalink';
2119        }
2120        if ( $navUrls['info'] ?? null ) {
2121            $toolbox['info'] = $navUrls['info'];
2122            $toolbox['info']['id'] = 't-info';
2123        }
2124
2125        return $toolbox;
2126    }
2127
2128    /**
2129     * Return an array of indicator data.
2130     * Can be used by subclasses but should not be extended.
2131     * @param array $indicators return value of OutputPage::getIndicators
2132     * @return array
2133     */
2134    protected function getIndicatorsData( $indicators ) {
2135        $indicatorData = [];
2136        foreach ( $indicators as $id => $content ) {
2137            $indicatorData[] = [
2138                'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
2139                'class' => 'mw-indicator',
2140                'html' => $content,
2141            ];
2142        }
2143        return $indicatorData;
2144    }
2145
2146    /**
2147     * Create an array of personal tools items from the data in the quicktemplate
2148     * stored by SkinTemplate.
2149     * The resulting array is built according to a format intended to be passed
2150     * through makeListItem to generate the html.
2151     * This is in reality the same list as already stored in personal_urls
2152     * however it is reformatted so that you can just pass the individual items
2153     * to makeListItem instead of hardcoding the element creation boilerplate.
2154     * @since 1.35
2155     * @param array $urls
2156     * @param bool $applyClassesToListItems (optional) when set behaves consistently with other menus,
2157     *   applying the `class` property applies to list items. When not set will move the class to child links.
2158     * @return array[]
2159     */
2160    final public function getPersonalToolsForMakeListItem( $urls, $applyClassesToListItems = false ) {
2161        $personal_tools = [];
2162        foreach ( $urls as $key => $plink ) {
2163            # The class on a personal_urls item is meant to go on the <a> instead
2164            # of the <li> so we have to use a single item "links" array instead
2165            # of using most of the personal_url's keys directly.
2166            $ptool = [
2167                'links' => [
2168                    [ 'single-id' => "pt-$key" ],
2169                ],
2170                'id' => "pt-$key",
2171                'icon' => $plink[ 'icon' ] ?? null,
2172            ];
2173            if ( $applyClassesToListItems && isset( $plink['class'] ) ) {
2174                $ptool['class'] = $plink['class'];
2175            }
2176            if ( isset( $plink['active'] ) ) {
2177                $ptool['active'] = $plink['active'];
2178            }
2179            // Set class for the link to link-class, when defined.
2180            // This allows newer notifications content navigation to retain their classes
2181            // when merged back into the personal tools.
2182            // Doing this here allows the loop below to overwrite the class if defined directly.
2183            if ( isset( $plink['link-class'] ) ) {
2184                $ptool['links'][0]['class'] = $plink['link-class'];
2185            }
2186            $props = [
2187                'href',
2188                'text',
2189                'dir',
2190                'data',
2191                'exists',
2192                'data-mw',
2193                'link-html',
2194            ];
2195            if ( !$applyClassesToListItems ) {
2196                $props[] = 'class';
2197            }
2198            foreach ( $props as $k ) {
2199                if ( isset( $plink[$k] ) ) {
2200                    $ptool['links'][0][$k] = $plink[$k];
2201                }
2202            }
2203            $personal_tools[$key] = $ptool;
2204        }
2205        return $personal_tools;
2206    }
2207
2208    /**
2209     * Makes a link, usually used by makeListItem to generate a link for an item
2210     * in a list used in navigation lists, portlets, portals, sidebars, etc...
2211     *
2212     * @since 1.35
2213     * @param string $key Usually a key from the list you are generating this
2214     * link from.
2215     * @param array $item Contains some of a specific set of keys.
2216     *
2217     * If "html" key is present, this will be returned. All other keys will be ignored.
2218     *
2219     * The text of the link will be generated either from the contents of the
2220     * "text" key in the $item array, if a "msg" key is present a message by
2221     * that name will be used, and if neither of those are set the $key will be
2222     * used as a message name.
2223     *
2224     * If a "href" key is not present makeLink will just output htmlescaped text.
2225     * The "href", "id", "class", "rel", and "type" keys are used as attributes
2226     * for the link if present.
2227     *
2228     * If an "id" or "single-id" (if you don't want the actual id to be output
2229     * on the link) is present it will be used to generate a tooltip and
2230     * accesskey for the link.
2231     *
2232     * The 'link-html' key can be used to prepend additional HTML inside the link HTML.
2233     * For example to prepend an icon.
2234     *
2235     * The keys "context" and "primary" are ignored; these keys are used
2236     * internally by skins and are not supposed to be included in the HTML
2237     * output.
2238     *
2239     * If you don't want an accesskey, set $item['tooltiponly'] = true;
2240     *
2241     * If a "data" key is present, it must be an array, where the keys represent
2242     * the data-xxx properties with their provided values. For example,
2243     *     $item['data'] = [
2244     *       'foo' => 1,
2245     *       'bar' => 'baz',
2246     *     ];
2247     * will render as element properties:
2248     *     data-foo='1' data-bar='baz'
2249     *
2250     * The "class" key currently accepts both a string and an array of classes, but this will be
2251     * changed to only accept an array in the future.
2252     *
2253     * @param array $linkOptions Can be used to affect the output of a link.
2254     * Possible options are:
2255     *   - 'text-wrapper' key to specify a list of elements to wrap the text of
2256     *   a link in. This should be an array of arrays containing a 'tag' and
2257     *   optionally an 'attributes' key. If you only have one element you don't
2258     *   need to wrap it in another array. eg: To use <a><span>...</span></a>
2259     *   in all links use [ 'text-wrapper' => [ 'tag' => 'span' ] ]
2260     *   for your options.
2261     *   - 'link-class' key can be used to specify additional classes to apply
2262     *   to all links.
2263     *   - 'link-fallback' can be used to specify a tag to use instead of "<a>"
2264     *   if there is no link. eg: If you specify 'link-fallback' => 'span' than
2265     *   any non-link will output a "<span>" instead of just text.
2266     *
2267     * @return string
2268     */
2269    final public function makeLink( $key, $item, $linkOptions = [] ) {
2270        $options = $linkOptions + $this->defaultLinkOptions;
2271        $component = new SkinComponentLink(
2272            $key, $item, $this->getContext(), $options
2273        );
2274        return $component->getTemplateData()[ 'html' ];
2275    }
2276
2277    /**
2278     * Generates a list item for a navigation, portlet, portal, sidebar... list
2279     *
2280     * @since 1.35
2281     * @param string $key Usually a key from the list you are generating this link from.
2282     * @param array $item Array of list item data containing some of a specific set of keys.
2283     *   The "id", "class" and "itemtitle" keys will be used as attributes for the list item,
2284     *   if "active" contains a value of true an "active" class will also be appended to class.
2285     *   The "class" key currently accepts both a string and an array of classes, but this will be
2286     *   changed to only accept an array in the future.
2287     *   For further options see the $item parameter of {@link SkinComponentLink::makeLink()}.
2288     * @phan-param array{id?:string,html?:string,class?:string|string[],itemtitle?:string,active?:bool} $item
2289     *
2290     * @param array $options
2291     * @phan-param array{tag?:string} $options
2292     *
2293     * If you want something other than a "<li>" you can pass a tag name such as
2294     * "tag" => "span" in the $options array to change the tag used.
2295     * link/content data for the list item may come in one of two forms
2296     * A "links" key may be used, in which case it should contain an array with
2297     * a list of links to include inside the list item, see makeLink for the
2298     * format of individual links array items.
2299     *
2300     * Otherwise the relevant keys from the list item $item array will be passed
2301     * to makeLink instead. Note however that "id" and "class" are used by the
2302     * list item directly so they will not be passed to makeLink
2303     * (however the link will still support a tooltip and accesskey from it)
2304     * If you need an id or class on a single link you should include a "links"
2305     * array with just one link item inside of it. You can also set "link-class" in
2306     * $item to set a class on the link itself. If you want to add a title
2307     * to the list item itself, you can set "itemtitle" to the value.
2308     * $options is also passed on to makeLink calls
2309     *
2310     * @return string
2311     */
2312    final public function makeListItem( $key, $item, $options = [] ) {
2313        $component = new SkinComponentListItem(
2314            $key, $item, $this->getContext(), $options, $this->defaultLinkOptions
2315        );
2316        return $component->getTemplateData()[ 'html-item' ];
2317    }
2318
2319    /**
2320     * Allows extensions to hook into known portlets and add stuff to them.
2321     * Unlike its BaseTemplate counterpart, this method does not wrap the html
2322     * provided by the hook in a div.
2323     *
2324     * @param string $name
2325     *
2326     * @return string html
2327     * @since 1.35
2328     */
2329    public function getAfterPortlet( string $name ): string {
2330        $html = '';
2331
2332        $this->getHookRunner()->onSkinAfterPortlet( $this, $name, $html );
2333
2334        return $html;
2335    }
2336
2337    /**
2338     * Prepare the subtitle of the page for output in the skin if one has been set.
2339     * @since 1.35
2340     * @param bool $withContainer since 1.40, when provided the mw-content-subtitle element will be output too.
2341     * @return string HTML
2342     */
2343    final public function prepareSubtitle( bool $withContainer = true ) {
2344        $out = $this->getOutput();
2345        $subpagestr = $this->subPageSubtitleInternal();
2346        if ( $subpagestr !== '' ) {
2347            $subpagestr = Html::rawElement( 'div', [ 'class' => 'subpages' ], $subpagestr );
2348        }
2349        $html = $subpagestr . $out->getSubtitle();
2350        return $withContainer ? Html::rawElement( 'div', [
2351            'id' => 'mw-content-subtitle',
2352        ] + $this->getUserLanguageAttributes(), $html ) : $html;
2353    }
2354
2355    /**
2356     * Returns array of config variables that should be added only to this skin
2357     * for use in JavaScript.
2358     * Skins can override this to add variables to the page.
2359     * @since 1.38 or 1.35 if extending SkinTemplate.
2360     * @return array
2361     */
2362    protected function getJsConfigVars(): array {
2363        return [];
2364    }
2365
2366    /**
2367     * Get user language attribute links array
2368     *
2369     * @return array HTML attributes
2370     */
2371    final protected function getUserLanguageAttributes() {
2372        $userLang = $this->getLanguage();
2373        $userLangCode = $userLang->getHtmlCode();
2374        $userLangDir = $userLang->getDir();
2375        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2376        if (
2377            $userLangCode !== $contLang->getHtmlCode() ||
2378            $userLangDir !== $contLang->getDir()
2379        ) {
2380            return [
2381                'lang' => $userLangCode,
2382                'dir' => $userLangDir,
2383            ];
2384        }
2385        return [];
2386    }
2387
2388    /**
2389     * Prepare user language attribute links
2390     * @since 1.38 on Skin and 1.35 on classes extending SkinTemplate
2391     * @return string HTML attributes
2392     */
2393    final protected function prepareUserLanguageAttributes() {
2394        return Html::expandAttributes(
2395            $this->getUserLanguageAttributes()
2396        );
2397    }
2398
2399    /**
2400     * Prepare undelete link for output in page.
2401     * @since 1.38 on Skin and 1.35 on classes extending SkinTemplate
2402     * @return null|string HTML, or null if there is no undelete link.
2403     */
2404    final protected function prepareUndeleteLink() {
2405        $undelete = $this->getUndeleteLink();
2406        return $undelete === '' ? null : '<div class="subpages">' . $undelete . '</div>';
2407    }
2408
2409    /**
2410     * Wrap the body text with language information and identifiable element
2411     *
2412     * @since 1.38 in Skin, previously was a method of SkinTemplate
2413     * @param Title $title
2414     * @param string $html body text
2415     * @return string html
2416     */
2417    protected function wrapHTML( $title, $html ) {
2418        // This wraps the "real" body content (i.e. parser output or special page).
2419        // On page views, elements like categories and contentSub are outside of this.
2420        return Html::rawElement( 'div', [
2421            'id' => 'mw-content-text',
2422            'class' => [
2423                'mw-body-content',
2424            ],
2425        ], $html );
2426    }
2427
2428    /**
2429     * Get current skin's options
2430     *
2431     * For documentation about supported options, refer to the Skin constructor.
2432     *
2433     * @internal Please call SkinFactory::getSkinOptions instead. See Skin::__construct for documentation.
2434     * @return array
2435     */
2436    final public function getOptions(): array {
2437        return $this->options + [
2438            'styles' => [],
2439            'scripts' => [],
2440            'toc' => true,
2441            'format' => 'html',
2442            'bodyClasses' => [],
2443            'clientPrefEnabled' => false,
2444            'responsive' => false,
2445            'supportsMwHeading' => false,
2446            'link' => [],
2447            'tempUserBanner' => false,
2448            'wrapSiteNotice' => false,
2449            'menus' => [
2450                // Legacy keys that are enabled by default for backwards compatibility
2451                'namespaces',
2452                'views',
2453                'actions',
2454                'variants',
2455                // Opt-in menus
2456                // * 'associated-pages'
2457                // * 'notifications'
2458                // * 'user-interface-preferences',
2459                // * 'user-page',
2460                // * 'user-menu',
2461            ]
2462        ];
2463    }
2464
2465    /**
2466     * Does the skin support the named menu? e.g. has it declared that it
2467     * will render a menu with the given ID?
2468     *
2469     * @since 1.39
2470     * @param string $menu See Skin::__construct for menu names.
2471     * @return bool
2472     */
2473    public function supportsMenu( string $menu ): bool {
2474        $options = $this->getOptions();
2475        return in_array( $menu, $options['menus'] );
2476    }
2477
2478    /**
2479     * Returns skin options for portlet links, used by addPortletLink
2480     *
2481     * @internal
2482     * @param RL\Context $context
2483     * @return array $linkOptions
2484     *   - 'text-wrapper' key to specify a list of elements to wrap the text of
2485     *   a link in. This should be an array of arrays containing a 'tag' and
2486     *   optionally an 'attributes' key. If you only have one element you don't
2487     *   need to wrap it in another array. eg: To use <a><span>...</span></a>
2488     *   in all links use [ 'text-wrapper' => [ 'tag' => 'span' ] ]
2489     *   for your options. If text-wrapper contains multiple entries they are
2490     *   interpreted as going from the outer wrapper to the inner wrapper.
2491     */
2492    public static function getPortletLinkOptions( RL\Context $context ): array {
2493        $skinName = $context->getSkin();
2494        $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2495        $options = $skinFactory->getSkinOptions( $skinName );
2496        $portletLinkOptions = $options['link'] ?? [];
2497        // Normalize link options to always have this key
2498        $portletLinkOptions += [ 'text-wrapper' => [] ];
2499        // Normalize text-wrapper to always be an array of arrays
2500        if ( isset( $portletLinkOptions['text-wrapper']['tag'] ) ) {
2501            $portletLinkOptions['text-wrapper'] = [ $portletLinkOptions['text-wrapper'] ];
2502        }
2503        return $portletLinkOptions;
2504    }
2505
2506    /**
2507     * @param string $name of the portal e.g. p-personal the name is personal.
2508     * @param array $items that are accepted input to Skin::makeListItem
2509     *
2510     * @return array data that can be passed to a Mustache template that
2511     *   represents a single menu.
2512     */
2513    final protected function getPortletData( string $name, array $items ): array {
2514        $portletComponent = new SkinComponentMenu(
2515            $name,
2516            $items,
2517            $this->getContext(),
2518            '',
2519            $this->defaultLinkOptions,
2520            $this->getAfterPortlet( $name )
2521        );
2522        return $portletComponent->getTemplateData();
2523    }
2524}