MediaWiki master
SkinComponentFooter.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Skin;
4
8use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
14
16 use ProtectedHookAccessorTrait;
17
19 private $skinContext;
20
21 public function __construct( SkinComponentRegistryContext $skinContext ) {
22 $this->skinContext = $skinContext;
23 }
24
28 private function getTemplateDataFooter(): array {
29 $data = [
30 'info' => $this->formatFooterInfoData(
31 $this->getFooterInfoData()
32 ),
33 'places' => $this->getSiteFooterLinks(),
34 ];
35 $skin = $this->skinContext->getContextSource()->getSkin();
36 foreach ( $data as $key => $existingItems ) {
37 $newItems = [];
38 $this->getHookRunner()->onSkinAddFooterLinks( $skin, $key, $newItems );
39 foreach ( $newItems as $index => $linkHTML ) {
40 $data[ $key ][ $index ] = [
41 'id' => 'footer-' . $key . '-' . $index,
42 'html' => $linkHTML,
43 ];
44 }
45 }
46 return $data;
47 }
48
52 public function getTemplateData(): array {
53 $footerData = $this->getTemplateDataFooter();
54
55 // Create the menu components from the footer data.
56 $footerInfoMenuData = new SkinComponentMenu(
57 'footer-info',
58 $footerData['info'],
59 $this->skinContext->getMessageLocalizer()
60 );
61 $footerSiteMenuData = new SkinComponentMenu(
62 'footer-places',
63 $footerData['places'],
64 $this->skinContext->getMessageLocalizer()
65 );
66
67 // To conform the footer menu data to the current SkinMustache specification,
68 // run the derived data through a cleanup function to unset unexpected data properties
69 // until the spec is updated to reflect the new properties introduced by the menu component.
70 // See https://www.mediawiki.org/wiki/Manual:SkinMustache.php#DataFooter
71 $footerMenuData = [];
72 $footerMenuData['data-info'] = $footerInfoMenuData->getTemplateData();
73 $footerMenuData['data-places'] = $footerSiteMenuData->getTemplateData();
74 $footerMenuData['data-icons'] = $this->getFooterIcons();
75 $footerMenuData = $this->formatFooterDataForCurrentSpec( $footerMenuData );
76
77 return [
78 'data-info' => $footerMenuData['data-info'],
79 'data-places' => $footerMenuData['data-places'],
80 'data-icons' => $footerMenuData['data-icons']
81 ];
82 }
83
94 private function getFooterInfoData(): array {
95 $action = null;
96 $skinContext = $this->skinContext;
97 $out = $skinContext->getOutput();
98 $ctx = $skinContext->getContextSource();
99 // This needs to be the relevant Title rather than just the raw Title for e.g. special pages that render content
100 $title = $skinContext->getRelevantTitle();
101 $titleExists = $title && $title->exists();
102 $config = $skinContext->getConfig();
103 $maxCredits = $config->get( MainConfigNames::MaxCredits );
104 $showCreditsIfMax = $config->get( MainConfigNames::ShowCreditsIfMax );
105 $useCredits = $titleExists
106 && $out->isArticle()
107 && $out->isRevisionCurrent()
108 && $maxCredits !== 0;
109
111 if ( $useCredits ) {
112 $article = Article::newFromWikiPage( $skinContext->getWikiPage(), $ctx );
113 $action = Action::factory( 'credits', $article, $ctx );
114 }
115
116 '@phan-var CreditsAction $action';
117 return [
118 'lastmod' => !$useCredits ? $this->lastModified() : null,
119 'numberofwatchingusers' => null,
120 'credits' => $useCredits && $action ?
121 $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
122 'copyright' => $titleExists &&
123 $out->showsCopyright() ? $this->getCopyright() : null,
124 ];
125 }
126
130 private function getCopyright() {
131 $copyright = new SkinComponentCopyright( $this->skinContext );
132 return $copyright->getTemplateData()[ 'html' ];
133 }
134
144 private function formatFooterInfoData( array $data ): array {
145 $formattedData = [];
146 foreach ( $data as $key => $item ) {
147 if ( $item ) {
148 $formattedData[ $key ] = [
149 'id' => 'footer-info-' . $key,
150 'html' => $item
151 ];
152 }
153 }
154 return $formattedData;
155 }
156
163 private function getSiteFooterLinks(): array {
164 $siteLinksData = [];
165 $siteLinks = [
166 'privacy' => [ 'privacy', 'privacypage' ],
167 'about' => [ 'aboutsite', 'aboutpage' ],
168 'disclaimers' => [ 'disclaimers', 'disclaimerpage' ]
169 ];
170 $localizer = $this->skinContext->getMessageLocalizer();
171
172 foreach ( $siteLinks as $key => $siteLink ) {
173 // Check if the link description has been disabled in the default language.
174 // If disabled, it is disabled for all languages.
175 if ( !$localizer->msg( $siteLink[0] )->inContentLanguage()->isDisabled() ) {
176 // Display the link for the user, described in their language (which may or may not be the same as the
177 // default language), but make the link target be the one site-wide page.
178 $title = Title::newFromText( $localizer->msg( $siteLink[1] )->inContentLanguage()->text() );
179 if ( $title !== null ) {
180 $siteLinksData[$key] = [
181 'id' => "footer-places-$key",
182 'text' => $localizer->msg( $siteLink[0] )->text(),
183 'href' => $title->fixSpecialName()->getLinkURL()
184 ];
185 }
186 }
187 }
188 return $siteLinksData;
189 }
190
202 public static function makeFooterIconHTML( Config $config, $icon, string $withImage = 'withImage' ): string {
203 if ( is_string( $icon ) ) {
204 $html = $icon;
205 } else { // Assuming array
206 $url = $icon['url'] ?? null;
207 unset( $icon['url'] );
208
209 $sources = '';
210 if ( isset( $icon['sources'] ) ) {
211 foreach ( $icon['sources'] as $source ) {
212 $sources .= Html::element( 'source', $source );
213 }
214 unset( $icon['sources'] );
215 }
216
217 if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
218 // Lazy-load footer icons, since they're not part of the printed view.
219 $icon['loading'] = 'lazy';
220 // do this the lazy way, just pass icon data as an attribute array
221 $html = Html::element( 'img', $icon );
222 if ( $sources ) {
223 $html = Html::openElement( 'picture' ) . $sources . $html . Html::closeElement( 'picture' );
224 }
225 } else {
226 $html = htmlspecialchars( $icon['alt'] ?? '' );
227 }
228 if ( $url ) {
229 $html = Html::rawElement(
230 'a',
231 [
232 'href' => $url,
233 // Using a fake Codex link button, as this is the long-expected UX; our apologies.
234 'class' => [
235 'cdx-button', 'cdx-button--fake-button',
236 'cdx-button--size-large', 'cdx-button--fake-button--enabled'
237 ],
238 'target' => $config->get( MainConfigNames::ExternalLinkTarget ),
239 ],
240 $html
241 );
242 }
243 }
244 return $html;
245 }
246
254 public static function getFooterIconsData( Config $config ) {
255 $footericons = [];
256 foreach (
257 $config->get( MainConfigNames::FooterIcons ) as $footerIconsKey => &$footerIconsBlock
258 ) {
259 if ( count( $footerIconsBlock ) > 0 ) {
260 $footericons[$footerIconsKey] = [];
261 foreach ( $footerIconsBlock as &$footerIcon ) {
262 if ( isset( $footerIcon['src'] ) ) {
263 if ( !isset( $footerIcon['width'] ) ) {
264 $footerIcon['width'] = 88;
265 }
266 if ( !isset( $footerIcon['height'] ) ) {
267 $footerIcon['height'] = 31;
268 }
269 }
270
271 // Only output icons which have an image.
272 // For historic reasons this mimics the `icononly` option
273 // for BaseTemplate::getFooterIcons.
274 // In some cases the icon may be an empty array.
275 // Filter these out. (See T269776)
276 if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) {
277 $footericons[$footerIconsKey][] = $footerIcon;
278 }
279 }
280
281 // If no valid icons with images were added, unset the parent array
282 // Should also prevent empty arrays from when no copyright is set.
283 if ( !count( $footericons[$footerIconsKey] ) ) {
284 unset( $footericons[$footerIconsKey] );
285 }
286 }
287 }
288 return $footericons;
289 }
290
298 private function getFooterIcons(): array {
299 $dataIcons = [];
300 $skinContext = $this->skinContext;
301 $config = $skinContext->getConfig();
302 // If footer icons are enabled append to the end of the rows
303 $footerIcons = self::getFooterIconsData(
304 $config
305 );
306
307 if ( count( $footerIcons ) > 0 ) {
308 $icons = [];
309 foreach ( $footerIcons as $blockName => $blockIcons ) {
310 $html = '';
311 foreach ( $blockIcons as $icon ) {
312 $html .= self::makeFooterIconHTML(
313 $config, $icon
314 );
315 }
316 // For historic reasons this mimics the `icononly` option
317 // for BaseTemplate::getFooterIcons. Empty rows should not be output.
318 if ( $html ) {
319 $block = htmlspecialchars( $blockName );
320 $icons[$block] = [
321 'name' => $block,
322 'id' => 'footer-' . $block . 'ico',
323 'html' => $html,
324 'class' => [ 'noprint' ],
325 ];
326 }
327 }
328
329 // Empty rows should not be output.
330 // This is how Vector has behaved historically but we can revisit later if necessary.
331 if ( count( $icons ) > 0 ) {
332 $dataIcons = new SkinComponentMenu(
333 'footer-icons',
334 $icons,
335 $this->skinContext->getMessageLocalizer(),
336 '',
337 []
338 );
339 }
340 }
341
342 return $dataIcons ? $dataIcons->getTemplateData() : [];
343 }
344
356 private function formatFooterDataForCurrentSpec( array $data ): array {
357 $formattedData = [];
358 foreach ( $data as $key => $item ) {
359 unset( $item['html-tooltip'] );
360 unset( $item['html-items'] );
361 unset( $item['html-after-portal'] );
362 unset( $item['html-before-portal'] );
363 unset( $item['label'] );
364 unset( $item['class'] );
365 foreach ( $item['array-items'] ?? [] as $index => $arrayItem ) {
366 unset( $item['array-items'][$index]['html-item'] );
367 }
368 $formattedData[$key] = $item;
369 $formattedData[$key]['className'] = $key === 'data-icons' ? 'noprint' : null;
370 }
371 return $formattedData;
372 }
373
380 private function lastModified() {
381 $skinContext = $this->skinContext;
382 $out = $skinContext->getOutput();
383 $timestamp = $out->getRevisionTimestamp();
384
385 // No cached timestamp, load it from the database
386 // TODO: This code shouldn't be necessary, revision ID should always be available
387 // Move this logic to OutputPage::getRevisionTimestamp if needed.
388 if ( $timestamp === null ) {
389 $revId = $out->getRevisionId();
390 if ( $revId !== null ) {
391 $timestamp = MediaWikiServices::getInstance()->getRevisionLookup()->getTimestampFromId( $revId );
392 }
393 }
394
395 $lastModified = new SkinComponentLastModified(
396 $skinContext,
397 $timestamp
398 );
399
400 return $lastModified->getTemplateData()['text'];
401 }
402}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Actions are things which can be done to pages (edit, delete, rollback, etc).
Definition Action.php:66
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:77
getTemplateData()
This returns all the data that is needed to the component.Returned array must be serialized....
__construct(SkinComponentRegistryContext $skinContext)
static makeFooterIconHTML(Config $config, $icon, string $withImage='withImage')
Renders a $wgFooterIcons icon according to the method's arguments.
static getFooterIconsData(Config $config)
Get data representation of icons.
getConfig()
Returns the config needed for the component.Config
Represents a title within MediaWiki.
Definition Title.php:78
Interface for configuration instances.
Definition Config.php:32
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$source
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...