Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.13% covered (warning)
57.13%
629 / 1101
42.03% covered (danger)
42.03%
29 / 69
CRAP
0.00% covered (danger)
0.00%
0 / 1
Skin
57.13% covered (warning)
57.13%
629 / 1101
42.03% covered (danger)
42.03%
29 / 69
7056.93
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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 makeFooterIcon
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 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.39% covered (warning)
63.39%
71 / 112
0.00% covered (danger)
0.00%
0 / 1
45.74
 buildFeedUrls
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
4.54
 appendSpecialPagesLinkIfAbsent
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 buildSidebar
100.00% covered (success)
100.00%
39 / 39
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
 createSidebarItem
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
10
 addToSidebarPlain
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
9
 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 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 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\HookContainer\ProtectedHookAccessorTrait;
23use MediaWiki\Html\Html;
24use MediaWiki\Language\Language;
25use MediaWiki\Language\LanguageCode;
26use MediaWiki\Logger\LoggerFactory;
27use MediaWiki\MainConfigNames;
28use MediaWiki\MediaWikiServices;
29use MediaWiki\Output\OutputPage;
30use MediaWiki\Parser\Sanitizer;
31use MediaWiki\ResourceLoader as RL;
32use MediaWiki\Revision\RevisionStore;
33use MediaWiki\Skin\SkinComponent;
34use MediaWiki\Skin\SkinComponentFooter;
35use MediaWiki\Skin\SkinComponentLink;
36use MediaWiki\Skin\SkinComponentListItem;
37use MediaWiki\Skin\SkinComponentMenu;
38use MediaWiki\Skin\SkinComponentRegistry;
39use MediaWiki\Skin\SkinComponentRegistryContext;
40use MediaWiki\Skin\SkinComponentUtils;
41use MediaWiki\SpecialPage\SpecialPage;
42use MediaWiki\Specials\SpecialUserRights;
43use MediaWiki\Title\Title;
44use MediaWiki\Title\TitleValue;
45use MediaWiki\User\User;
46use MediaWiki\User\UserIdentity;
47use MediaWiki\User\UserIdentityValue;
48use MediaWiki\WikiMap\WikiMap;
49use Wikimedia\ObjectCache\WANObjectCache;
50use Wikimedia\Rdbms\IDBAccessObject;
51
52/**
53 * @defgroup Skins Skins
54 */
55
56/**
57 * The base class for all skins.
58 *
59 * See docs/Skin.md for more information.
60 *
61 * @stable to extend
62 * @ingroup Skins
63 */
64abstract class Skin extends ContextSource {
65    use ProtectedHookAccessorTrait;
66
67    /**
68     * @var array link options used in Skin::makeLink. Can be set by skin option `link`.
69     */
70    private $defaultLinkOptions;
71
72    /**
73     * @var string|null
74     */
75    protected $skinname = null;
76
77    /**
78     * @var array Skin options passed into constructor
79     */
80    protected $options = [];
81    /** @var Title|null */
82    protected $mRelevantTitle = null;
83
84    /**
85     * @var UserIdentity|null|false
86     */
87    private $mRelevantUser = false;
88
89    /** The current major version of the skin specification. */
90    protected const VERSION_MAJOR = 1;
91
92    /** @var array|null Cache for getLanguages() */
93    private $languageLinks;
94
95    /** @var array|null Cache for buildSidebar() */
96    private $sidebar;
97
98    /**
99     * @var SkinComponentRegistry Initialised in constructor.
100     */
101    private $componentRegistry = null;
102
103    /**
104     * Get the current major version of Skin. This is used to manage changes
105     * to underlying data and for providing support for older and new versions of code.
106     *
107     * @since 1.36
108     * @return int
109     */
110    public static function getVersion() {
111        return self::VERSION_MAJOR;
112    }
113
114    /**
115     * @internal use in Skin.php, SkinTemplate.php or SkinMustache.php
116     * @param string $name
117     * @return SkinComponent
118     */
119    final protected function getComponent( string $name ): SkinComponent {
120        return $this->componentRegistry->getComponent( $name );
121    }
122
123    /**
124     * @stable to extend. Subclasses may extend this method to add additional
125     * template data.
126     * @internal this method should never be called outside Skin and its subclasses
127     * as it can be computationally expensive and typically has side effects on the Skin
128     * instance, through execution of hooks.
129     *
130     * The data keys should be valid English words. Compound words should
131     * be hyphenated except if they are normally written as one word. Each
132     * key should be prefixed with a type hint, this may be enforced by the
133     * class PHPUnit test.
134     *
135     * Plain strings are prefixed with 'html-', plain arrays with 'array-'
136     * and complex array data with 'data-'. 'is-' and 'has-' prefixes can
137     * be used for boolean variables.
138     * Messages are prefixed with 'msg-', followed by their message key.
139     * All messages specified in the skin option 'messages' will be available
140     * under 'msg-MESSAGE-NAME'.
141     *
142     * @return array Data for a mustache template
143     */
144    public function getTemplateData() {
145        $title = $this->getTitle();
146        $out = $this->getOutput();
147        $user = $this->getUser();
148        $isMainPage = $title->isMainPage();
149        $blankedHeading = false;
150        // Heading can only be blanked on "views". It should
151        // still show on action=edit, diff pages and action=history
152        $isHeadingOverridable = $this->getContext()->getActionName() === 'view' &&
153            !$this->getRequest()->getRawVal( 'diff' );
154
155        if ( $isMainPage && $isHeadingOverridable ) {
156            // Special casing for the main page to allow more freedom to editors, to
157            // design their home page differently. This came up in T290480.
158            // The parameter for logged in users is optional and may
159            // or may not be used.
160            $titleMsg = $user->isAnon() ?
161                $this->msg( 'mainpage-title' ) :
162                $this->msg( 'mainpage-title-loggedin', $user->getName() );
163
164            // T298715: Use content language rather than user language so that
165            // the custom page heading is shown to all users, not just those that have
166            // their interface set to the site content language.
167            //
168            // T331095: Avoid Message::inContentLanuguage and, just like Parser,
169            // pick the language variant based on the current URL and/or user
170            // preference if their variant relates to the content language.
171            $forceUIMsgAsContentMsg = $this->getConfig()
172                ->get( MainConfigNames::ForceUIMsgAsContentMsg );
173            if ( !in_array( $titleMsg->getKey(), (array)$forceUIMsgAsContentMsg ) ) {
174                $services = MediaWikiServices::getInstance();
175                $contLangVariant = $services->getLanguageConverterFactory()
176                    ->getLanguageConverter( $services->getContentLanguage() )
177                    ->getPreferredVariant();
178                $titleMsg->inLanguage( $contLangVariant );
179            }
180            $titleMsg->setInterfaceMessageFlag( true );
181            $blankedHeading = $titleMsg->isBlank();
182            if ( !$titleMsg->isDisabled() ) {
183                $htmlTitle = $titleMsg->parse();
184            } else {
185                $htmlTitle = $out->getPageTitle();
186            }
187        } else {
188            $htmlTitle = $out->getPageTitle();
189        }
190
191        $data = [
192            // raw HTML
193            'html-title-heading' => Html::rawElement(
194                'h1',
195                [
196                    'id' => 'firstHeading',
197                    'class' => 'firstHeading mw-first-heading',
198                    'style' => $blankedHeading ? 'display: none' : null
199                ] + $this->getUserLanguageAttributes(),
200                $htmlTitle
201            ),
202            'html-title' => $htmlTitle ?: null,
203            // Boolean values
204            'is-title-blank' => $blankedHeading, // @since 1.38
205            'is-anon' => $user->isAnon(),
206            'is-article' => $out->isArticle(),
207            'is-mainpage' => $isMainPage,
208            'is-specialpage' => $title->isSpecialPage(),
209            'canonical-url' => $this->getCanonicalUrl(),
210        ];
211
212        $components = $this->componentRegistry->getComponents();
213        foreach ( $components as $componentName => $component ) {
214            $data['data-' . $componentName] = $component->getTemplateData();
215        }
216        return $data;
217    }
218
219    /**
220     * Normalize a skin preference value to a form that can be loaded.
221     *
222     * If a skin can't be found, it will fall back to the configured default ($wgDefaultSkin), or the
223     * hardcoded default ($wgFallbackSkin) if the default skin is unavailable too.
224     *
225     * @param string $key 'monobook', 'vector', etc.
226     * @return string
227     */
228    public static function normalizeKey( string $key ) {
229        $config = MediaWikiServices::getInstance()->getMainConfig();
230        $defaultSkin = $config->get( MainConfigNames::DefaultSkin );
231        $fallbackSkin = $config->get( MainConfigNames::FallbackSkin );
232        $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
233        $skinNames = $skinFactory->getInstalledSkins();
234
235        // Make keys lowercase for case-insensitive matching.
236        $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
237        $key = strtolower( $key );
238        $defaultSkin = strtolower( $defaultSkin );
239        $fallbackSkin = strtolower( $fallbackSkin );
240
241        if ( $key == '' || $key == 'default' ) {
242            // Don't return the default immediately;
243            // in a misconfiguration we need to fall back.
244            $key = $defaultSkin;
245        }
246
247        if ( isset( $skinNames[$key] ) ) {
248            return $key;
249        }
250
251        // Older versions of the software used a numeric setting
252        // in the user preferences.
253        $fallback = [
254            0 => $defaultSkin,
255            2 => 'cologneblue'
256        ];
257
258        if ( isset( $fallback[$key] ) ) {
259            // @phan-suppress-next-line PhanTypeMismatchDimFetch False positive
260            $key = $fallback[$key];
261        }
262
263        if ( isset( $skinNames[$key] ) ) {
264            return $key;
265        } elseif ( isset( $skinNames[$defaultSkin] ) ) {
266            return $defaultSkin;
267        } else {
268            return $fallbackSkin;
269        }
270    }
271
272    /**
273     * @since 1.31
274     * @param string|array|null $options Options for the skin can be an array (since 1.35).
275     *  When a string is passed, it's the skinname.
276     *  When an array is passed:
277     *
278     *  - `name`: Required. Internal skin name, generally in lowercase to comply with conventions
279     *     for interface message keys and CSS class names which embed this value.
280     *
281     *  - `format`: Enable rendering of skin as json or html.
282     *
283     *     Since: MW 1.43
284     *     Default: `html`
285     *
286     *  - `styles`: ResourceLoader style modules to load on all pages. Default: `[]`
287     *
288     *  - `scripts`: ResourceLoader script modules to load on all pages. Default: `[]`
289     *
290     *  - `toc`: Whether a table of contents is included in the main article content
291     *     area. If your skin has place a table of contents elsewhere (for example, the sidebar),
292     *     set this to `false`.
293     *
294     *     See ParserOutput::getText() for the implementation logic.
295     *
296     *     Default: `true`
297     *
298     *  - `bodyClasses`: An array of extra class names to add to the HTML `<body>` element.
299     *     Default: `[]`
300     *
301     *  - `clientPrefEnabled`: Enable support for mw.user.clientPrefs.
302     *     This instructs OutputPage and ResourceLoader\ClientHtml to include an inline script
303     *     in web responses for unregistered users to switch HTML classes as needed.
304     *
305     *     Since: MW 1.41
306     *     Default: `false`
307     *
308     *  - `wrapSiteNotice`: Enable support for standard site notice wrapper.
309     *     This instructs the Skin to wrap banners in div#siteNotice.
310     *
311     *     Since: MW 1.42
312     *     Default: `false`
313     *
314     *  - `responsive`: Whether the skin supports responsive behaviour and wants a viewport meta
315     *     tag to be added to the HTML head. Note, users can disable this feature via a user
316     *     preference.
317     *
318     *     Default: `false`
319     *
320     *  - `supportsMwHeading`: Whether the skin supports new HTML markup for headings, which uses
321     *     `<div class="mw-heading">` tags (https://www.mediawiki.org/wiki/Heading_HTML_changes).
322     *     If false, MediaWiki will output the legacy markup instead.
323     *
324     *     Since: MW 1.43
325     *     Default: `false` (will become `true` in and then will be removed in the future)
326     *
327     *  - `link`: An array of link option overriddes. See Skin::makeLink for the available options.
328     *
329     *     Default: `[]`
330     *
331     *  - `tempUserBanner`: Whether to display a banner on page views by in temporary user sessions.
332     *     This will prepend SkinComponentTempUserBanner to the `<body>` above everything else.
333     *     See also MediaWiki\MainConfigSchema::AutoCreateTempUser and User::isTemp.
334     *
335     *     Default: `false`
336     *
337     *  - `menus`: Which menus the skin supports, to allow features like SpecialWatchlist
338     *     to render their own navigation in the skins that don't support certain menus.
339     *     For any key in the array, the skin is promising to render an element e.g. the
340     *     presence of `associated-pages` means the skin will render a menu
341     *     compatible with mw.util.addPortletLink which has the ID p-associated-pages.
342     *
343     *     Default: `['namespaces', 'views', 'actions', 'variants']`
344     *
345     *     Opt-in menus:
346     *     - `associated-pages`
347     *     - `notifications`
348     *     - `user-interface-preferences`
349     *     - `user-page`
350     *     - `user-menu`
351     */
352    public function __construct( $options = null ) {
353        if ( is_string( $options ) ) {
354            $this->skinname = $options;
355        } elseif ( $options ) {
356            $name = $options['name'] ?? null;
357
358            if ( !$name ) {
359                throw new SkinException( 'Skin name must be specified' );
360            }
361
362            // Defaults are set in Skin::getOptions()
363            $this->options = $options;
364            $this->skinname = $name;
365        }
366        $this->defaultLinkOptions = $this->getOptions()['link'];
367        $this->componentRegistry = new SkinComponentRegistry(
368            new SkinComponentRegistryContext( $this )
369        );
370    }
371
372    /**
373     * @return string|null Skin name
374     */
375    public function getSkinName() {
376        return $this->skinname;
377    }
378
379    /**
380     * Indicates if this skin is responsive.
381     * Responsive skins have skin--responsive added to <body> by OutputPage,
382     * and a viewport <meta> tag set by Skin::initPage.
383     *
384     * @since 1.36
385     * @stable to override
386     * @return bool
387     */
388    public function isResponsive() {
389        $isSkinResponsiveCapable = $this->getOptions()['responsive'];
390        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
391
392        return $isSkinResponsiveCapable &&
393            $userOptionsLookup->getBoolOption( $this->getUser(), 'skin-responsive' );
394    }
395
396    /**
397     * @stable to override
398     * @param OutputPage $out
399     */
400    public function initPage( OutputPage $out ) {
401        $skinMetaTags = $this->getConfig()->get( MainConfigNames::SkinMetaTags );
402        $siteName = $this->getConfig()->get( MainConfigNames::Sitename );
403        $this->preloadExistence();
404
405        if ( $this->isResponsive() ) {
406            $out->addMeta(
407                'viewport',
408                'width=device-width, initial-scale=1.0, ' .
409                'user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0'
410            );
411        } else {
412            // Force the desktop experience on an iPad by resizing the mobile viewport to
413            // the value of @min-width-breakpoint-desktop (1120px).
414            // This is as @min-width-breakpoint-desktop-wide usually tends to optimize
415            // for larger screens with max-widths and margins.
416            // The initial-scale SHOULD NOT be set here as defining it will impact zoom
417            // on mobile devices. To allow font-size adjustment in iOS devices (see T311795)
418            // we will define a zoom in JavaScript on certain devices (see resources/src/mediawiki.page.ready/ready.js)
419            $out->addMeta(
420                'viewport',
421                'width=1120'
422            );
423        }
424
425        $tags = [
426            'og:site_name' => $siteName,
427            'og:title' => $out->getHTMLTitle(),
428            'twitter:card' => 'summary_large_image',
429            'og:type' => 'website',
430        ];
431
432        // Support sharing on platforms such as Facebook and Twitter
433        foreach ( $tags as $key => $value ) {
434            if ( in_array( $key, $skinMetaTags ) ) {
435                $out->addMeta( $key, $value );
436            }
437        }
438    }
439
440    /**
441     * Defines the ResourceLoader modules that should be added to the skin
442     * It is recommended that skins wishing to override call parent::getDefaultModules()
443     * and substitute out any modules they wish to change by using a key to look them up
444     *
445     * Any modules defined with the 'styles' key will be added as render blocking CSS via
446     * Output::addModuleStyles. Similarly, each key should refer to a list of modules
447     *
448     * @stable to override
449     * @return array Array of modules with helper keys for easy overriding
450     */
451    public function getDefaultModules() {
452        $out = $this->getOutput();
453        $user = $this->getUser();
454
455        // Modules declared in the $modules literal are loaded
456        // for ALL users, on ALL pages, in ALL skins.
457        // Keep this list as small as possible!
458        $modules = [
459            // The 'styles' key sets render-blocking style modules
460            // Unlike other keys in $modules, this is an associative array
461            // where each key is its own group pointing to a list of modules
462            'styles' => [
463                'skin' => $this->getOptions()['styles'],
464                'core' => [],
465                'content' => [],
466                'syndicate' => [],
467                'user' => []
468            ],
469            'core' => [
470                'site',
471                'mediawiki.page.ready',
472            ],
473            // modules that enhance the content in some way
474            'content' => [],
475            // modules relating to search functionality
476            'search' => [],
477            // Skins can register their own scripts
478            'skin' => $this->getOptions()['scripts'],
479            // modules relating to functionality relating to watching an article
480            'watch' => [],
481            // modules which relate to the current users preferences
482            'user' => [],
483            // modules relating to RSS/Atom Feeds
484            'syndicate' => [],
485        ];
486
487        // Preload jquery.tablesorter for mediawiki.page.ready
488        if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
489            $modules['content'][] = 'jquery.tablesorter';
490            $modules['styles']['content'][] = 'jquery.tablesorter.styles';
491        }
492
493        // Preload jquery.makeCollapsible for mediawiki.page.ready
494        if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
495            $modules['content'][] = 'jquery.makeCollapsible';
496            $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
497        }
498
499        // Load relevant styles on wiki pages that use mw-ui-button.
500        // Since 1.26, this no longer loads unconditionally. Special pages
501        // and extensions should load this via addModuleStyles() instead.
502        if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
503            $modules['styles']['content'][] = 'mediawiki.ui.button';
504        }
505        // Since 1.41, styling for mw-message-box is only required for
506        // messages that appear in article content.
507        // This should only be removed when a suitable alternative exists
508        // e.g. https://phabricator.wikimedia.org/T363607 is resolved.
509        if ( strpos( $out->getHTML(), 'mw-message-box' ) !== false ) {
510            $modules['styles']['content'][] = 'mediawiki.legacy.messageBox';
511        }
512        // If the page is using Codex message box markup load Codex styles.
513        // Since 1.41. Skins can unset this if they prefer to handle this via other
514        // means.
515        // This is intended for extensions.
516        // For content, this should not be considered stable, and will likely
517        // be removed when https://phabricator.wikimedia.org/T363607 is resolved.
518        if ( strpos( $out->getHTML(), 'cdx-message' ) !== false ) {
519            // This channel will be used to identify pages relying on this method that
520            // shouldn't be.
521            $logger = LoggerFactory::getInstance( 'SkinCodex' );
522            $codexModules = array_filter( $out->getModuleStyles(), static function ( $module ) {
523                return strpos( $module, 'codex' ) !== false;
524            } );
525            if ( !$codexModules ) {
526                $logger->warning( 'Page uses Codex markup without appropriate style pack.' );
527                $modules['styles']['content'][] = 'mediawiki.codex.messagebox.styles';
528            }
529        }
530
531        if ( $out->isTOCEnabled() ) {
532            $modules['content'][] = 'mediawiki.toc';
533        }
534
535        $authority = $this->getAuthority();
536        $relevantTitle = $this->getRelevantTitle();
537        if ( $authority->getUser()->isRegistered()
538            && $authority->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' )
539            && $relevantTitle && $relevantTitle->canExist()
540        ) {
541            $modules['watch'][] = 'mediawiki.page.watch.ajax';
542        }
543
544        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
545        if ( $userOptionsLookup->getBoolOption( $user, 'editsectiononrightclick' )
546            || ( $out->isArticle() && $userOptionsLookup->getOption( $user, 'editondblclick' ) )
547        ) {
548            $modules['user'][] = 'mediawiki.misc-authed-pref';
549        }
550
551        if ( $out->isSyndicated() ) {
552            $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
553        }
554
555        if ( $user->isTemp() ) {
556            $modules['user'][] = 'mediawiki.tempUserBanner';
557            $modules['styles']['user'][] = 'mediawiki.tempUserBanner.styles';
558        }
559
560        if ( $this->getTitle() && $this->getTitle()->getNamespace() === NS_FILE ) {
561            $modules['styles']['core'][] = 'filepage'; // local Filepage.css, T31277, T356505
562        }
563
564        return $modules;
565    }
566
567    /**
568     * Preload the existence of three commonly-requested pages in a single query
569     */
570    private function preloadExistence() {
571        $titles = [];
572
573        // User/talk link
574        $user = $this->getUser();
575        if ( $user->isRegistered() ) {
576            $titles[] = $user->getUserPage();
577            $titles[] = $user->getTalkPage();
578        }
579
580        // Check, if the page can hold some kind of content, otherwise do nothing
581        $title = $this->getRelevantTitle();
582        if ( $title && $title->canExist() && $title->canHaveTalkPage() ) {
583            $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
584            if ( $title->isTalkPage() ) {
585                $titles[] = $namespaceInfo->getSubjectPage( $title );
586            } else {
587                $titles[] = $namespaceInfo->getTalkPage( $title );
588            }
589        }
590
591        // Preload for self::getCategoryLinks
592        $allCats = $this->getOutput()->getCategoryLinks();
593        if ( isset( $allCats['normal'] ) && $allCats['normal'] !== [] ) {
594            $catLink = Title::newFromText( $this->msg( 'pagecategorieslink' )->inContentLanguage()->text() );
595            if ( $catLink ) {
596                // If this is a special page, the LinkBatch would skip it
597                $titles[] = $catLink;
598            }
599        }
600
601        $this->getHookRunner()->onSkinPreloadExistence( $titles, $this );
602
603        if ( $titles ) {
604            $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
605            $lb = $linkBatchFactory->newLinkBatch( $titles );
606            $lb->setCaller( __METHOD__ );
607            $lb->execute();
608        }
609    }
610
611    /**
612     * @see self::getRelevantTitle()
613     * @param Title $t
614     */
615    public function setRelevantTitle( $t ) {
616        $this->mRelevantTitle = $t;
617    }
618
619    /**
620     * Return the "relevant" title.
621     * A "relevant" title is not necessarily the actual title of the page.
622     * Special pages like Special:MovePage use set the page they are acting on
623     * as their "relevant" title, this allows the skin system to display things
624     * such as content tabs which belong to that page instead of displaying
625     * a basic special page tab which has almost no meaning.
626     *
627     * @return Title|null the title is null when no relevant title was set, as this
628     *   falls back to ContextSource::getTitle
629     */
630    public function getRelevantTitle() {
631        return $this->mRelevantTitle ?? $this->getTitle();
632    }
633
634    /**
635     * @see self::getRelevantUser()
636     * @param UserIdentity|null $u
637     */
638    public function setRelevantUser( ?UserIdentity $u ) {
639        $this->mRelevantUser = $u;
640    }
641
642    /**
643     * Return the "relevant" user.
644     * A "relevant" user is similar to a relevant title. Special pages like
645     * Special:Contributions mark the user which they are relevant to so that
646     * things like the toolbox can display the information they usually are only
647     * able to display on a user's userpage and talkpage.
648     *
649     * @return UserIdentity|null Null if there's no relevant user or the viewer cannot view it.
650     */
651    public function getRelevantUser(): ?UserIdentity {
652        if ( $this->mRelevantUser === false ) {
653            $this->mRelevantUser = null; // false indicates we never attempted to load it.
654            $title = $this->getRelevantTitle();
655            if ( $title->hasSubjectNamespace( NS_USER ) ) {
656                $services = MediaWikiServices::getInstance();
657                $rootUser = $title->getRootText();
658                $userNameUtils = $services->getUserNameUtils();
659                if ( $userNameUtils->isIP( $rootUser ) ) {
660                    $this->mRelevantUser = UserIdentityValue::newAnonymous( $rootUser );
661                } else {
662                    $user = $services->getUserIdentityLookup()->getUserIdentityByName( $rootUser );
663                    $this->mRelevantUser = $user && $user->isRegistered() ? $user : null;
664                }
665            }
666        }
667
668        // The relevant user should only be set if it exists. However, if it exists but is hidden,
669        // and the viewer cannot see hidden users, this exposes the fact that the user exists;
670        // pretend like the user does not exist in such cases, by setting it to null. T120883
671        if ( $this->mRelevantUser && $this->mRelevantUser->isRegistered() ) {
672            $userBlock = MediaWikiServices::getInstance()
673                ->getBlockManager()
674                ->getBlock( $this->mRelevantUser, null );
675            if ( $userBlock && $userBlock->getHideName() &&
676                !$this->getAuthority()->isAllowed( 'hideuser' )
677            ) {
678                $this->mRelevantUser = null;
679            }
680        }
681
682        return $this->mRelevantUser;
683    }
684
685    /**
686     * Outputs the HTML for the page.
687     * @internal Only to be called by OutputPage.
688     */
689    final public function outputPageFinal( OutputPage $out ) {
690        // generate body
691        ob_start();
692        $this->outputPage();
693        $html = ob_get_contents();
694        ob_end_clean();
695
696        // T259955: OutputPage::headElement must be called last
697        // as it calls OutputPage::getRlClient, which freezes the ResourceLoader
698        // modules queue for the current page load.
699        // Since Skins can add ResourceLoader modules via OutputPage::addModule
700        // and OutputPage::addModuleStyles changing this order can lead to
701        // bugs.
702        $head = $out->headElement( $this );
703        $tail = $out->tailElement( $this );
704
705        echo $head . $html . $tail;
706    }
707
708    /**
709     * Outputs the HTML generated by other functions.
710     */
711    abstract public function outputPage();
712
713    /**
714     * TODO: document
715     * @param Title $title
716     * @return string
717     */
718    public function getPageClasses( $title ) {
719        $services = MediaWikiServices::getInstance();
720        $ns = $title->getNamespace();
721        $numeric = 'ns-' . $ns;
722
723        if ( $title->isSpecialPage() ) {
724            $type = 'ns-special';
725            // T25315: provide a class based on the canonical special page name without subpages
726            [ $canonicalName ] = $services->getSpecialPageFactory()->resolveAlias( $title->getDBkey() );
727            if ( $canonicalName ) {
728                $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
729            } else {
730                $type .= ' mw-invalidspecialpage';
731            }
732        } else {
733            if ( $title->isTalkPage() ) {
734                $type = 'ns-talk';
735            } else {
736                $type = 'ns-subject';
737            }
738            // T208315: add HTML class when the user can edit the page
739            if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) {
740                $type .= ' mw-editable';
741            }
742        }
743
744        $titleFormatter = $services->getTitleFormatter();
745        $name = Sanitizer::escapeClass( 'page-' . $titleFormatter->getPrefixedText( $title ) );
746        $root = Sanitizer::escapeClass( 'rootpage-' . $titleFormatter->formatTitle( $ns, $title->getRootText() ) );
747        // Add a static class that is not subject to translation to allow extensions/skins/global code to target main
748        // pages reliably (T363281)
749        if ( $title->isMainPage() ) {
750            $name .= ' page-Main_Page';
751        }
752
753        return "$numeric $type $name $root";
754    }
755
756    /**
757     * Return values for <html> element
758     * @return array Array of associative name-to-value elements for <html> element
759     */
760    public function getHtmlElementAttributes() {
761        $lang = $this->getLanguage();
762        return [
763            'lang' => $lang->getHtmlCode(),
764            'dir' => $lang->getDir(),
765            'class' => 'client-nojs',
766        ];
767    }
768
769    /**
770     * @return string HTML
771     */
772    public function getCategoryLinks() {
773        $out = $this->getOutput();
774        $allCats = $out->getCategoryLinks();
775        $title = $this->getTitle();
776        $services = MediaWikiServices::getInstance();
777        $linkRenderer = $services->getLinkRenderer();
778
779        if ( $allCats === [] ) {
780            return '';
781        }
782
783        $embed = "<li>";
784        $pop = "</li>";
785
786        $s = '';
787        $colon = $this->msg( 'colon-separator' )->escaped();
788
789        if ( !empty( $allCats['normal'] ) ) {
790            $t = $embed . implode( $pop . $embed, $allCats['normal'] ) . $pop;
791
792            $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) );
793            $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
794            $pageCategoriesLinkTitle = Title::newFromText( $linkPage );
795            if ( $pageCategoriesLinkTitle ) {
796                $link = $linkRenderer->makeLink( $pageCategoriesLinkTitle, $msg->text() );
797            } else {
798                $link = $msg->escaped();
799            }
800            $s .= Html::rawElement(
801                'div',
802                [ 'id' => 'mw-normal-catlinks', 'class' => 'mw-normal-catlinks' ],
803                $link . $colon . Html::rawElement( 'ul', [], $t )
804            );
805        }
806
807        # Hidden categories
808        if ( isset( $allCats['hidden'] ) ) {
809            $userOptionsLookup = $services->getUserOptionsLookup();
810
811            if ( $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' ) ) {
812                $class = ' mw-hidden-cats-user-shown';
813            } elseif ( $title->inNamespace( NS_CATEGORY ) ) {
814                $class = ' mw-hidden-cats-ns-shown';
815            } else {
816                $class = ' mw-hidden-cats-hidden';
817            }
818
819            $s .= Html::rawElement(
820                'div',
821                [ 'id' => 'mw-hidden-catlinks', 'class' => "mw-hidden-catlinks$class" ],
822                $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
823                    $colon .
824                    Html::rawElement(
825                        'ul',
826                        [],
827                        $embed . implode( $pop . $embed, $allCats['hidden'] ) . $pop
828                    )
829            );
830        }
831
832        return $s;
833    }
834
835    /**
836     * @return string HTML
837     */
838    public function getCategories() {
839        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
840        $showHiddenCats = $userOptionsLookup->getBoolOption( $this->getUser(), 'showhiddencats' );
841
842        $catlinks = $this->getCategoryLinks();
843        // Check what we're showing
844        $allCats = $this->getOutput()->getCategoryLinks();
845        $showHidden = $showHiddenCats || $this->getTitle()->inNamespace( NS_CATEGORY );
846
847        $classes = [ 'catlinks' ];
848        if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
849            $classes[] = 'catlinks-allhidden';
850        }
851
852        return Html::rawElement(
853            'div',
854            [ 'id' => 'catlinks', 'class' => $classes, 'data-mw' => 'interface' ],
855            $catlinks
856        );
857    }
858
859    /**
860     * This runs a hook to allow extensions placing their stuff after content
861     * and article metadata (e.g. categories).
862     * Note: This function has nothing to do with afterContent().
863     *
864     * This hook is placed here in order to allow using the same hook for all
865     * skins, both the SkinTemplate based ones and the older ones, which directly
866     * use this class to get their data.
867     *
868     * The output of this function gets processed in SkinTemplate::outputPage() for
869     * the SkinTemplate based skins, all other skins should directly echo it.
870     *
871     * @return string Empty by default, if not changed by any hook function.
872     */
873    protected function afterContentHook() {
874        $data = '';
875
876        if ( $this->getHookRunner()->onSkinAfterContent( $data, $this ) ) {
877            // adding just some spaces shouldn't toggle the output
878            // of the whole <div/>, so we use trim() here
879            if ( trim( $data ) != '' ) {
880                // Doing this here instead of in the skins to
881                // ensure that the div has the same ID in all
882                // skins
883                $data = "<div id='mw-data-after-content'>\n" .
884                    "\t$data\n" .
885                    "</div>\n";
886            }
887        } else {
888            wfDebug( "Hook SkinAfterContent changed output processing." );
889        }
890
891        return $data;
892    }
893
894    /**
895     * Get the canonical URL (permalink) for the page including oldid if present.
896     *
897     * @return string
898     */
899    private function getCanonicalUrl() {
900        $title = $this->getTitle();
901        $oldid = $this->getOutput()->getRevisionId();
902        if ( $oldid ) {
903            return $title->getCanonicalURL( 'oldid=' . $oldid );
904        } else {
905            // oldid not available for non existing pages
906            return $title->getCanonicalURL();
907        }
908    }
909
910    /**
911     * Text with the permalink to the source page,
912     * usually shown on the footer of a printed page
913     *
914     * @stable to override
915     * @return string HTML text with an URL
916     */
917    public function printSource() {
918        $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
919        $url = htmlspecialchars( $urlUtils->expandIRI( $this->getCanonicalUrl() ) ?? '' );
920
921        return $this->msg( 'retrievedfrom' )
922            ->rawParams( '<a dir="ltr" href="' . $url . '">' . $url . '</a>' )
923            ->parse();
924    }
925
926    /**
927     * @return string HTML
928     */
929    public function getUndeleteLink() {
930        $action = $this->getRequest()->getRawVal( 'action' ) ?? 'view';
931        $title = $this->getTitle();
932        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
933
934        if ( ( !$title->exists() || $action == 'history' ) &&
935            $this->getAuthority()->probablyCan( 'deletedhistory', $title )
936        ) {
937            $n = $title->getDeletedEditsCount();
938
939            if ( $n ) {
940                if ( $this->getAuthority()->probablyCan( 'undelete', $title ) ) {
941                    $msg = 'thisisdeleted';
942                } else {
943                    $msg = 'viewdeleted';
944                }
945
946                $subtitle = $this->msg( $msg )->rawParams(
947                    $linkRenderer->makeKnownLink(
948                        SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ),
949                        $this->msg( 'restorelink' )->numParams( $n )->text() )
950                    )->escaped();
951
952                $links = [];
953                // Add link to page logs, unless we're on the history page (which
954                // already has one)
955                if ( $action !== 'history' ) {
956                    $links[] = $linkRenderer->makeKnownLink(
957                        SpecialPage::getTitleFor( 'Log' ),
958                        $this->msg( 'viewpagelogs-lowercase' )->text(),
959                        [],
960                        [ 'page' => $title->getPrefixedText() ]
961                    );
962                }
963
964                // Allow extensions to add more links
965                $this->getHookRunner()->onUndeletePageToolLinks(
966                    $this->getContext(), $linkRenderer, $links );
967
968                if ( $links ) {
969                    $subtitle .= ''
970                        . $this->msg( 'word-separator' )->escaped()
971                        . $this->msg( 'parentheses' )
972                            ->rawParams( $this->getLanguage()->pipeList( $links ) )
973                            ->escaped();
974                }
975
976                return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
977            }
978        }
979
980        return '';
981    }
982
983    /**
984     * @return string
985     */
986    private function subPageSubtitleInternal() {
987        $services = MediaWikiServices::getInstance();
988        $linkRenderer = $services->getLinkRenderer();
989        $out = $this->getOutput();
990        $title = $out->getTitle();
991        $subpages = '';
992
993        if ( !$this->getHookRunner()->onSkinSubPageSubtitle( $subpages, $this, $out ) ) {
994            return $subpages;
995        }
996
997        $hasSubpages = $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() );
998        if ( !$out->isArticle() || !$hasSubpages ) {
999            return $subpages;
1000        }
1001
1002        $ptext = $title->getPrefixedText();
1003        if ( strpos( $ptext, '/' ) !== false ) {
1004            $links = explode( '/', $ptext );
1005            array_pop( $links );
1006            $count = 0;
1007            $growingLink = '';
1008            $display = '';
1009            $lang = $this->getLanguage();
1010
1011            foreach ( $links as $link ) {
1012                $growingLink .= $link;
1013                $display .= $link;
1014                $linkObj = Title::newFromText( $growingLink );
1015
1016                if ( $linkObj && $linkObj->isKnown() ) {
1017                    $getlink = $linkRenderer->makeKnownLink( $linkObj, $display );
1018
1019                    $count++;
1020
1021                    if ( $count > 1 ) {
1022                        $subpages .= $this->msg( 'pipe-separator' )->escaped();
1023                    } else {
1024                        $subpages .= '&lt; ';
1025                    }
1026
1027                    $subpages .= Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ], $getlink );
1028                    $display = '';
1029                } else {
1030                    $display .= '/';
1031                }
1032                $growingLink .= '/';
1033            }
1034        }
1035
1036        return $subpages;
1037    }
1038
1039    /**
1040     * Helper function for mapping template data for use in legacy function
1041     *
1042     * @param string $dataKey
1043     * @param string $name
1044     * @return string
1045     */
1046    private function getFooterTemplateDataItem( string $dataKey, string $name ) {
1047        $footerData = $this->getComponent( 'footer' )->getTemplateData();
1048        $items = $footerData[ $dataKey ]['array-items'] ?? [];
1049        foreach ( $items as $item ) {
1050            if ( $item['name'] === $name ) {
1051                return $item['html'];
1052            }
1053        }
1054        return '';
1055    }
1056
1057    final public function getCopyright(): string {
1058        return $this->getFooterTemplateDataItem( 'data-info', 'copyright' );
1059    }
1060
1061    /**
1062     * @param string $align
1063     * @return string
1064     */
1065    public function logoText( $align = '' ) {
1066        if ( $align != '' ) {
1067            $a = " style='float: {$align};'";
1068        } else {
1069            $a = '';
1070        }
1071
1072        $mp = $this->msg( 'mainpage' )->escaped();
1073        $url = htmlspecialchars( Title::newMainPage()->getLocalURL() );
1074
1075        $logourl = RL\SkinModule::getAvailableLogos(
1076            $this->getConfig(),
1077            $this->getLanguage()->getCode()
1078        )[ '1x' ];
1079        return "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
1080    }
1081
1082    /**
1083     * Get template representation of the footer.
1084     *
1085     * Stable to use since 1.40 but should not be overridden.
1086     *
1087     * @since 1.35
1088     * @internal for use inside SkinComponentRegistryContext
1089     * @return array
1090     */
1091    final public function getFooterIcons() {
1092        return SkinComponentFooter::getFooterIconsData(
1093            $this->getConfig()
1094        );
1095    }
1096
1097    /**
1098     * Renders a $wgFooterIcons icon according to the method's arguments
1099     *
1100     * Stable to use since 1.40 but should not be overridden.
1101     *
1102     * @param array $icon The icon to build the html for, see $wgFooterIcons
1103     *   for the format of this array.
1104     * @param bool|string $withImage Whether to use the icon's image or output
1105     *   a text-only footericon.
1106     * @return string HTML
1107     */
1108    final public function makeFooterIcon( $icon, $withImage = 'withImage' ) {
1109        return SkinComponentFooter::makeFooterIconHTML(
1110            $this->getConfig(), $icon, $withImage
1111        );
1112    }
1113
1114    /**
1115     * Return URL options for the 'edit page' link.
1116     * This may include an 'oldid' specifier, if the current page view is such.
1117     *
1118     * @return array
1119     * @internal
1120     */
1121    public function editUrlOptions() {
1122        $options = [ 'action' => 'edit' ];
1123        $out = $this->getOutput();
1124
1125        if ( !$out->isRevisionCurrent() ) {
1126            $options['oldid'] = intval( $out->getRevisionId() );
1127        }
1128
1129        return $options;
1130    }
1131
1132    /**
1133     * @param UserIdentity|int $id
1134     * @return bool
1135     */
1136    public function showEmailUser( $id ) {
1137        if ( $id instanceof UserIdentity ) {
1138            $targetUser = User::newFromIdentity( $id );
1139        } else {
1140            $targetUser = User::newFromId( $id );
1141        }
1142
1143        # The sending user must have a confirmed email address and the receiving
1144        # user must accept emails from the sender.
1145        $emailUser = MediaWikiServices::getInstance()->getEmailUserFactory()
1146            ->newEmailUser( $this->getUser() );
1147
1148        return $emailUser->canSend()->isOK()
1149            && $emailUser->validateTarget( $targetUser )->isOK();
1150    }
1151
1152    /* these are used extensively in SkinTemplate, but also some other places */
1153
1154    /**
1155     * @param string|array $urlaction
1156     * @return string
1157     */
1158    public static function makeMainPageUrl( $urlaction = '' ) {
1159        $title = Title::newMainPage();
1160
1161        return $title->getLinkURL( $urlaction );
1162    }
1163
1164    /**
1165     * Make a URL for a Special Page using the given query and protocol.
1166     *
1167     * If $proto is set to null, make a local URL. Otherwise, make a full
1168     * URL with the protocol specified.
1169     *
1170     * @deprecated since 1.39 - Moved to SkinComponentUtils::makeSpecialUrl
1171     * @param string $name Name of the Special page
1172     * @param string|array $urlaction Query to append
1173     * @param string|null $proto Protocol to use or null for a local URL
1174     * @return string
1175     */
1176    public static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
1177        wfDeprecated( __METHOD__, '1.39' );
1178        return SkinComponentUtils::makeSpecialUrl( $name, $urlaction, $proto );
1179    }
1180
1181    /**
1182     * @deprecated since 1.39 - Moved to SkinComponentUtils::makeSpecialUrlSubpage
1183     * @param string $name
1184     * @param string|bool $subpage false for no subpage
1185     * @param string|array $urlaction
1186     * @return string
1187     */
1188    public static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
1189        wfDeprecated( __METHOD__, '1.39' );
1190        return SkinComponentUtils::makeSpecialUrlSubpage( $name, $subpage, $urlaction );
1191    }
1192
1193    /**
1194     * If url string starts with http, consider as external URL, else
1195     * internal
1196     * @param string $name
1197     * @return string URL
1198     */
1199    public static function makeInternalOrExternalUrl( $name ) {
1200        $protocols = MediaWikiServices::getInstance()->getUrlUtils()->validProtocols();
1201
1202        if ( preg_match( '/^(?i:' . $protocols . ')/', $name ) ) {
1203            return $name;
1204        } else {
1205            $title = $name instanceof Title ? $name : Title::newFromText( $name );
1206            return $title ? $title->getLocalURL() : '';
1207        }
1208    }
1209
1210    /**
1211     * these return an array with the 'href' and boolean 'exists'
1212     * @param string|Title $name
1213     * @param string|array $urlaction
1214     * @return array
1215     */
1216    protected static function makeUrlDetails( $name, $urlaction = '' ) {
1217        $title = $name instanceof Title ? $name : Title::newFromText( $name );
1218        return [
1219            'href' => $title ? $title->getLocalURL( $urlaction ) : '',
1220            'exists' => $title && $title->isKnown(),
1221        ];
1222    }
1223
1224    /**
1225     * Make URL details where the article exists (or at least it's convenient to think so)
1226     * @param string|Title $name Article name
1227     * @param string|array $urlaction
1228     * @return array
1229     */
1230    protected static function makeKnownUrlDetails( $name, $urlaction = '' ) {
1231        $title = $name instanceof Title ? $name : Title::newFromText( $name );
1232        return [
1233            'href' => $title ? $title->getLocalURL( $urlaction ) : '',
1234            'exists' => (bool)$title,
1235        ];
1236    }
1237
1238    /**
1239     * Allows correcting the language of interlanguage links which, mostly due to
1240     * legacy reasons, do not always match the standards compliant language tag.
1241     *
1242     * @param string $code
1243     * @return string
1244     * @since 1.35
1245     */
1246    public function mapInterwikiToLanguage( $code ) {
1247        $map = $this->getConfig()->get( MainConfigNames::InterlanguageLinkCodeMap );
1248        return $map[ $code ] ?? $code;
1249    }
1250
1251    /**
1252     * Generates array of language links for the current page.
1253     * This may includes items added to this section by the SidebarBeforeOutput hook
1254     * (which may not necessarily be language links)
1255     *
1256     * @since 1.35
1257     * @return array
1258     */
1259    public function getLanguages() {
1260        if ( $this->getConfig()->get( MainConfigNames::HideInterlanguageLinks ) ) {
1261            return [];
1262        }
1263        if ( $this->languageLinks === null ) {
1264            $hookRunner = $this->getHookRunner();
1265
1266            $userLang = $this->getLanguage();
1267            $languageLinks = [];
1268            $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
1269
1270            foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) {
1271                [ $prefix, $title ] = explode( ':', $languageLinkText, 2 );
1272                $class = 'interlanguage-link interwiki-' . $prefix;
1273
1274                [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
1275                $languageLinkTitle = TitleValue::tryNew( NS_MAIN, $title, $frag, $prefix );
1276                if ( $languageLinkTitle === null ) {
1277                    continue;
1278                }
1279                $ilInterwikiCode = $this->mapInterwikiToLanguage( $prefix );
1280
1281                $ilLangName = $langNameUtils->getLanguageName( $ilInterwikiCode );
1282
1283                if ( strval( $ilLangName ) === '' ) {
1284                    $ilDisplayTextMsg = $this->msg( "interlanguage-link-$ilInterwikiCode" );
1285                    if ( !$ilDisplayTextMsg->isDisabled() ) {
1286                        // Use custom MW message for the display text
1287                        $ilLangName = $ilDisplayTextMsg->text();
1288                    } else {
1289                        // Last resort: fallback to the language link target
1290                        $ilLangName = $languageLinkText;
1291                    }
1292                } else {
1293                    // Use the language autonym as display text
1294                    $ilLangName = $this->getLanguage()->ucfirst( $ilLangName );
1295                }
1296
1297                // CLDR extension or similar is required to localize the language name;
1298                // otherwise we'll end up with the autonym again.
1299                $ilLangLocalName =
1300                    $langNameUtils->getLanguageName( $ilInterwikiCode, $userLang->getCode() );
1301
1302                $languageLinkTitleText = $languageLinkTitle->getText();
1303                if ( $ilLangLocalName === '' ) {
1304                    $ilFriendlySiteName =
1305                        $this->msg( "interlanguage-link-sitename-$ilInterwikiCode" );
1306                    if ( !$ilFriendlySiteName->isDisabled() ) {
1307                        if ( $languageLinkTitleText === '' ) {
1308                            $ilTitle =
1309                                $this->msg( 'interlanguage-link-title-nonlangonly',
1310                                    $ilFriendlySiteName->text() )->text();
1311                        } else {
1312                            $ilTitle =
1313                                $this->msg( 'interlanguage-link-title-nonlang',
1314                                    $languageLinkTitleText, $ilFriendlySiteName->text() )->text();
1315                        }
1316                    } else {
1317                        // we have nothing friendly to put in the title, so fall back to
1318                        // displaying the interlanguage link itself in the title text
1319                        // (similar to what is done in page content)
1320                        $ilTitle = $languageLinkTitle->getInterwiki() . ":$languageLinkTitleText";
1321                    }
1322                } elseif ( $languageLinkTitleText === '' ) {
1323                    $ilTitle =
1324                        $this->msg( 'interlanguage-link-title-langonly', $ilLangLocalName )->text();
1325                } else {
1326                    $ilTitle =
1327                        $this->msg( 'interlanguage-link-title', $languageLinkTitleText,
1328                            $ilLangLocalName )->text();
1329                }
1330
1331                $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
1332                // A TitleValue is sufficient above this point, but we need
1333                // a full Title for ::getFullURL() and the hook invocation
1334                $languageLinkFullTitle = Title::newFromLinkTarget( $languageLinkTitle );
1335                $languageLink = [
1336                    'href' => $languageLinkFullTitle->getFullURL(),
1337                    'text' => $ilLangName,
1338                    'title' => $ilTitle,
1339                    'class' => $class,
1340                    'link-class' => 'interlanguage-link-target',
1341                    'lang' => $ilInterwikiCodeBCP47,
1342                    'hreflang' => $ilInterwikiCodeBCP47,
1343                    'data-title' => $languageLinkTitleText,
1344                    'data-language-autonym' => $ilLangName,
1345                    'data-language-local-name' => $ilLangLocalName,
1346                ];
1347                $hookRunner->onSkinTemplateGetLanguageLink(
1348                    $languageLink, $languageLinkFullTitle, $this->getTitle(), $this->getOutput()
1349                );
1350                $languageLinks[] = $languageLink;
1351            }
1352            $this->languageLinks = $languageLinks;
1353        }
1354
1355        return $this->languageLinks;
1356    }
1357
1358    /**
1359     * Build array of common navigation links.
1360     * Assumes thispage property has been set before execution.
1361     * @since 1.35
1362     * @return array
1363     */
1364    protected function buildNavUrls() {
1365        $out = $this->getOutput();
1366        $title = $this->getTitle();
1367        $thispage = $title->getPrefixedDBkey();
1368        $uploadNavigationUrl = $this->getConfig()->get( MainConfigNames::UploadNavigationUrl );
1369
1370        $nav_urls = [];
1371        $nav_urls['mainpage'] = [ 'href' => self::makeMainPageUrl() ];
1372        if ( $uploadNavigationUrl ) {
1373            $nav_urls['upload'] = [ 'href' => $uploadNavigationUrl ];
1374        } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getAuthority() ) === true ) {
1375            $nav_urls['upload'] = [ 'href' => SkinComponentUtils::makeSpecialUrl( 'Upload' ) ];
1376        } else {
1377            $nav_urls['upload'] = false;
1378        }
1379
1380        $nav_urls['print'] = false;
1381        $nav_urls['permalink'] = false;
1382        $nav_urls['info'] = false;
1383        $nav_urls['whatlinkshere'] = false;
1384        $nav_urls['recentchangeslinked'] = false;
1385        $nav_urls['contributions'] = false;
1386        $nav_urls['log'] = false;
1387        $nav_urls['blockip'] = false;
1388        $nav_urls['changeblockip'] = false;
1389        $nav_urls['unblockip'] = false;
1390        $nav_urls['mute'] = false;
1391        $nav_urls['emailuser'] = false;
1392        $nav_urls['userrights'] = false;
1393
1394        // A print stylesheet is attached to all pages, but nobody ever
1395        // figures that out. :)  Add a link...
1396        if ( !$out->isPrintable() && ( $out->isArticle() || $title->isSpecialPage() ) ) {
1397            $nav_urls['print'] = [
1398                'text' => $this->msg( 'printableversion' )->text(),
1399                'href' => 'javascript:print();'
1400            ];
1401        }
1402
1403        if ( $out->isArticle() ) {
1404            // Also add a "permalink" while we're at it
1405            $revid = $out->getRevisionId();
1406            if ( $revid ) {
1407                $nav_urls['permalink'] = [
1408                    'icon' => 'link',
1409                    'text' => $this->msg( 'permalink' )->text(),
1410                    'href' => $title->getLocalURL( "oldid=$revid" )
1411                ];
1412            }
1413        }
1414
1415        if ( $out->isArticleRelated() ) {
1416            $nav_urls['whatlinkshere'] = [
1417                'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $thispage )->getLocalURL()
1418            ];
1419
1420            $nav_urls['info'] = [
1421                'icon' => 'infoFilled',
1422                'text' => $this->msg( 'pageinfo-toolboxlink' )->text(),
1423                'href' => $title->getLocalURL( "action=info" )
1424            ];
1425
1426            if ( $title->exists() || $title->inNamespace( NS_CATEGORY ) ) {
1427                $nav_urls['recentchangeslinked'] = [
1428                    'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $thispage )->getLocalURL()
1429                ];
1430            }
1431        }
1432
1433        $user = $this->getRelevantUser();
1434
1435        if ( $user ) {
1436            $rootUser = $user->getName();
1437
1438            $nav_urls['contributions'] = [
1439                'text' => $this->msg( 'tool-link-contributions', $rootUser )->text(),
1440                'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Contributions', $rootUser ),
1441                'tooltip-params' => [ $rootUser ],
1442            ];
1443
1444            $nav_urls['log'] = [
1445                'icon' => 'listBullet',
1446                'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Log', $rootUser )
1447            ];
1448
1449            if ( $this->getAuthority()->isAllowed( 'block' ) ) {
1450                // Check if the user is already blocked
1451                $userBlock = MediaWikiServices::getInstance()
1452                ->getBlockManager()
1453                ->getBlock( $user, null );
1454                if ( $userBlock ) {
1455                    $nav_urls['changeblockip'] = [
1456                        'icon' => 'block',
1457                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Block', $rootUser )
1458                    ];
1459                    $nav_urls['unblockip'] = [
1460                        'icon' => 'unBlock',
1461                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Unblock', $rootUser )
1462                    ];
1463                } else {
1464                    $nav_urls['blockip'] = [
1465                        'icon' => 'block',
1466                        'text' => $this->msg( 'blockip', $rootUser )->text(),
1467                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Block', $rootUser )
1468                    ];
1469                }
1470            }
1471
1472            if ( $this->showEmailUser( $user ) ) {
1473                $nav_urls['emailuser'] = [
1474                    'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
1475                    'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
1476                    'tooltip-params' => [ $rootUser ],
1477                ];
1478            }
1479
1480            if ( $user->isRegistered() ) {
1481                if ( $this->getConfig()->get( MainConfigNames::EnableSpecialMute ) &&
1482                    $this->getUser()->isNamed()
1483                ) {
1484                    $nav_urls['mute'] = [
1485                        'text' => $this->msg( 'mute-preferences' )->text(),
1486                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Mute', $rootUser )
1487                    ];
1488                }
1489
1490                // Don't show links to Special:UserRights for temporary accounts (as they cannot have groups)
1491                $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
1492                if ( !$userNameUtils->isTemp( $user->getName() ) ) {
1493                    $sur = new SpecialUserRights;
1494                    $sur->setContext( $this->getContext() );
1495                    $canChange = $sur->userCanChangeRights( $user );
1496                    $delimiter = $this->getConfig()->get(
1497                        MainConfigNames::UserrightsInterwikiDelimiter );
1498                    if ( str_contains( $rootUser, $delimiter ) ) {
1499                        // Username contains interwiki delimiter, link it via the
1500                        // #{userid} syntax. (T260222)
1501                        $linkArgs = [ false, [ 'user' => '#' . $user->getId() ] ];
1502                    } else {
1503                        $linkArgs = [ $rootUser ];
1504                    }
1505                    $nav_urls['userrights'] = [
1506                        'icon' => 'userGroup',
1507                        'text' => $this->msg(
1508                            $canChange ? 'tool-link-userrights' : 'tool-link-userrights-readonly',
1509                            $rootUser
1510                        )->text(),
1511                        'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Userrights', ...$linkArgs )
1512                    ];
1513                }
1514            }
1515        }
1516
1517        return $nav_urls;
1518    }
1519
1520    /**
1521     * Build data structure representing syndication links.
1522     * @since 1.35
1523     * @return array
1524     */
1525    final protected function buildFeedUrls() {
1526        $feeds = [];
1527        $out = $this->getOutput();
1528        if ( $out->isSyndicated() ) {
1529            foreach ( $out->getSyndicationLinks() as $format => $link ) {
1530                $feeds[$format] = [
1531                    // Messages: feed-atom, feed-rss
1532                    'text' => $this->msg( "feed-$format" )->text(),
1533                    'href' => $link
1534                ];
1535            }
1536        }
1537        return $feeds;
1538    }
1539
1540    /**
1541     * Append link to SpecialPages into navigation sidebar if it doesn't already exist
1542     *
1543     * Created to help migrate sidebars after the SpecialPages link was removed from the toolbar.
1544     *
1545     * @since 1.44
1546     * @deprecated since 1.44 - will be hard deprecated in 1.45
1547     */
1548    private function appendSpecialPagesLinkIfAbsent() {
1549        if ( $this->sidebar === null ) {
1550            return;
1551        }
1552
1553        $isSpecialPagesPresent = false;
1554        foreach ( $this->sidebar as $bar ) {
1555            if ( in_array( 'n-specialpages', array_column( $bar, 'id' ) ) ) {
1556                $isSpecialPagesPresent = true;
1557                break;
1558            }
1559        }
1560        if ( !$isSpecialPagesPresent ) {
1561            $item = $this->createSidebarItem( 'specialpages-url', 'specialpages' );
1562            if ( $item !== null ) {
1563                $this->sidebar['navigation'][] = $item;
1564            }
1565        }
1566    }
1567
1568    /**
1569     * Build an array that represents the sidebar(s), the navigation bar among them.
1570     *
1571     * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
1572     *
1573     * The format of the returned array is [ heading => content, ... ], where:
1574     * - heading is the heading of a navigation portlet. It is either:
1575     *   - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
1576     *     (Note that 'SEARCH' unlike others is not supported out-of-the-box by the skins.
1577     *     For it to work, a skin must add custom support for it.)
1578     *   - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
1579     *   - plain text, which should be HTML-escaped by the skin
1580     * - content is the contents of the portlet.
1581     *   - For keys that aren't magic strings, this is an array of link data, where the
1582     *     array items are arrays in the format expected by the $item parameter of
1583     *     {@link self::makeListItem()}.
1584     *   - For magic strings, the format varies. For LANGUAGES and TOOLBOX it is the same as above;
1585     *     for SEARCH the value will be ignored.
1586     *
1587     * Note that extensions can control the sidebar contents using the SkinBuildSidebar hook
1588     * and can technically insert anything in here; skin creators are expected to handle
1589     * values described above.
1590     *
1591     * @return array
1592     */
1593    public function buildSidebar() {
1594        if ( $this->sidebar === null ) {
1595            $services = MediaWikiServices::getInstance();
1596            $callback = function ( $old = null, &$ttl = null ) {
1597                $bar = [];
1598                $this->addToSidebar( $bar, 'sidebar' );
1599
1600                // This hook may vary its behaviour by skin.
1601                $this->getHookRunner()->onSkinBuildSidebar( $this, $bar );
1602                $msgCache = MediaWikiServices::getInstance()->getMessageCache();
1603                if ( $msgCache->isDisabled() ) {
1604                    // Don't cache the fallback if DB query failed. T133069
1605                    $ttl = WANObjectCache::TTL_UNCACHEABLE;
1606                }
1607
1608                return $bar;
1609            };
1610
1611            $msgCache = $services->getMessageCache();
1612            $wanCache = $services->getMainWANObjectCache();
1613            $config = $this->getConfig();
1614            $languageCode = $this->getLanguage()->getCode();
1615
1616            $sidebar = $config->get( MainConfigNames::EnableSidebarCache )
1617                ? $wanCache->getWithSetCallback(
1618                    $wanCache->makeKey( 'sidebar', $languageCode, $this->getSkinName() ?? '' ),
1619                    $config->get( MainConfigNames::SidebarCacheExpiry ),
1620                    $callback,
1621                    [
1622                        'checkKeys' => [
1623                            // Unless there is both no exact $code override nor an i18n definition
1624                            // in the software, the only MediaWiki page to check is for $code.
1625                            $msgCache->getCheckKey( $languageCode )
1626                        ],
1627                        'lockTSE' => 30
1628                    ]
1629                )
1630                : $callback();
1631
1632            $sidebar['TOOLBOX'] = array_merge(