MediaWiki master
SkinComponentFooter.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Skin;
4
8use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
15
17 use ProtectedHookAccessorTrait;
18
20 private $skinContext;
21
22 public function __construct( SkinComponentRegistryContext $skinContext ) {
23 $this->skinContext = $skinContext;
24 }
25
29 private function getTemplateDataFooter(): array {
30 $data = [
31 'info' => $this->formatFooterInfoData(
32 $this->getFooterInfoData()
33 ),
34 'places' => $this->getSiteFooterLinks(),
35 ];
36 $skin = $this->skinContext->getContextSource()->getSkin();
37 foreach ( $data as $key => $existingItems ) {
38 $newItems = [];
39 $this->getHookRunner()->onSkinAddFooterLinks( $skin, $key, $newItems );
40 foreach ( $newItems as $index => $linkHTML ) {
41 $data[ $key ][ $index ] = [
42 'id' => 'footer-' . $key . '-' . $index,
43 'html' => $linkHTML,
44 ];
45 }
46 }
47 return $data;
48 }
49
53 public function getTemplateData(): array {
54 $footerData = $this->getTemplateDataFooter();
55
56 // Create the menu components from the footer data.
57 $footerInfoMenuData = new SkinComponentMenu(
58 'footer-info',
59 $footerData['info'],
60 $this->skinContext->getMessageLocalizer()
61 );
62 $footerSiteMenuData = new SkinComponentMenu(
63 'footer-places',
64 $footerData['places'],
65 $this->skinContext->getMessageLocalizer()
66 );
67
68 // To conform the footer menu data to the current SkinMustache specification,
69 // run the derived data through a cleanup function to unset unexpected data properties
70 // until the spec is updated to reflect the new properties introduced by the menu component.
71 // See https://www.mediawiki.org/wiki/Manual:SkinMustache.php#DataFooter
72 $footerMenuData = [];
73 $footerMenuData['data-info'] = $footerInfoMenuData->getTemplateData();
74 $footerMenuData['data-places'] = $footerSiteMenuData->getTemplateData();
75 $footerMenuData['data-icons'] = $this->getFooterIcons();
76 $footerMenuData = $this->formatFooterDataForCurrentSpec( $footerMenuData );
77
78 return [
79 'data-info' => $footerMenuData['data-info'],
80 'data-places' => $footerMenuData['data-places'],
81 'data-icons' => $footerMenuData['data-icons']
82 ];
83 }
84
95 private function getFooterInfoData(): array {
96 $action = null;
97 $skinContext = $this->skinContext;
98 $out = $skinContext->getOutput();
99 $ctx = $skinContext->getContextSource();
100 // This needs to be the relevant Title rather than just the raw Title for e.g. special pages that render content
101 $title = $skinContext->getRelevantTitle();
102 $titleExists = $title && $title->exists();
103 $config = $skinContext->getConfig();
104 $maxCredits = $config->get( MainConfigNames::MaxCredits );
105 $showCreditsIfMax = $config->get( MainConfigNames::ShowCreditsIfMax );
106 $useCredits = $titleExists
107 && $out->isArticle()
108 && $out->isRevisionCurrent()
109 && $maxCredits !== 0;
110
112 if ( $useCredits ) {
113 $article = Article::newFromWikiPage( $skinContext->getWikiPage(), $ctx );
114 $action = Action::factory( 'credits', $article, $ctx );
115 }
116
117 '@phan-var CreditsAction $action';
118 return [
119 'lastmod' => !$useCredits ? $this->lastModified() : null,
120 'numberofwatchingusers' => null,
121 'credits' => $useCredits && $action ?
122 $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
123 'renderedwith' => $this->renderedWith(),
124 // Copyright is often rendered as a block in skins and
125 // should thus be last, after the inline elements.
126 'copyright' => $titleExists &&
127 $out->showsCopyright() ? $this->getCopyright() : null,
128 ];
129 }
130
134 private function getCopyright() {
135 $copyright = new SkinComponentCopyright( $this->skinContext );
136 return $copyright->getTemplateData()[ 'html' ];
137 }
138
148 private function formatFooterInfoData( array $data ): array {
149 $formattedData = [];
150 foreach ( $data as $key => $item ) {
151 if ( $item ) {
152 $formattedData[ $key ] = [
153 'id' => 'footer-info-' . $key,
154 'html' => $item
155 ];
156 }
157 }
158 return $formattedData;
159 }
160
167 private function getSiteFooterLinks(): array {
168 $siteLinksData = [];
169 $siteLinks = [
170 'privacy' => [ 'privacy', 'privacypage' ],
171 'about' => [ 'aboutsite', 'aboutpage' ],
172 'disclaimers' => [ 'disclaimers', 'disclaimerpage' ]
173 ];
174 $localizer = $this->skinContext->getMessageLocalizer();
175
176 foreach ( $siteLinks as $key => $siteLink ) {
177 // Check if the link description has been disabled in the default language.
178 // If disabled, it is disabled for all languages.
179 if ( !$localizer->msg( $siteLink[0] )->inContentLanguage()->isDisabled() ) {
180 // Display the link for the user, described in their language (which may or may not be the same as the
181 // default language), but make the link target be the one site-wide page.
182 $title = Title::newFromText( $localizer->msg( $siteLink[1] )->inContentLanguage()->text() );
183 if ( $title !== null ) {
184 $siteLinksData[$key] = [
185 'id' => "footer-places-$key",
186 'text' => $localizer->msg( $siteLink[0] )->text(),
187 'href' => $title->fixSpecialName()->getLinkURL()
188 ];
189 }
190 }
191 }
192 return $siteLinksData;
193 }
194
206 public static function makeFooterIconHTML( Config $config, $icon, string $withImage = 'withImage' ): string {
207 if ( is_string( $icon ) ) {
208 $html = $icon;
209 } else { // Assuming array
210 $url = $icon['url'] ?? null;
211 unset( $icon['url'] );
212
213 $sources = '';
214 if ( isset( $icon['sources'] ) ) {
215 foreach ( $icon['sources'] as $source ) {
216 $sources .= Html::element( 'source', $source );
217 }
218 unset( $icon['sources'] );
219 }
220
221 if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
222 // Lazy-load footer icons, since they're not part of the printed view.
223 $icon['loading'] = 'lazy';
224 // do this the lazy way, just pass icon data as an attribute array
225 $html = Html::element( 'img', $icon );
226 if ( $sources ) {
227 $html = Html::openElement( 'picture' ) . $sources . $html . Html::closeElement( 'picture' );
228 }
229 } else {
230 $html = htmlspecialchars( $icon['alt'] ?? '' );
231 }
232 if ( $url ) {
233 $html = Html::rawElement(
234 'a',
235 [
236 'href' => $url,
237 // Using a fake Codex link button, as this is the long-expected UX; our apologies.
238 'class' => [
239 'cdx-button', 'cdx-button--fake-button',
240 'cdx-button--size-large', 'cdx-button--fake-button--enabled'
241 ],
242 'target' => $config->get( MainConfigNames::ExternalLinkTarget ),
243 ],
244 $html
245 );
246 }
247 }
248 return $html;
249 }
250
258 public static function getFooterIconsData( Config $config ) {
259 $footericons = [];
260 foreach (
261 $config->get( MainConfigNames::FooterIcons ) as $footerIconsKey => &$footerIconsBlock
262 ) {
263 if ( count( $footerIconsBlock ) > 0 ) {
264 $footericons[$footerIconsKey] = [];
265 foreach ( $footerIconsBlock as &$footerIcon ) {
266 if ( isset( $footerIcon['src'] ) ) {
267 if ( !isset( $footerIcon['width'] ) ) {
268 $footerIcon['width'] = 88;
269 }
270 if ( !isset( $footerIcon['height'] ) ) {
271 $footerIcon['height'] = 31;
272 }
273 }
274
275 // Only output icons which have an image.
276 // For historic reasons this mimics the `icononly` option
277 // for BaseTemplate::getFooterIcons.
278 // In some cases the icon may be an empty array.
279 // Filter these out. (See T269776)
280 if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) {
281 $footericons[$footerIconsKey][] = $footerIcon;
282 }
283 }
284
285 // If no valid icons with images were added, unset the parent array
286 // Should also prevent empty arrays from when no copyright is set.
287 if ( !count( $footericons[$footerIconsKey] ) ) {
288 unset( $footericons[$footerIconsKey] );
289 }
290 }
291 }
292 return $footericons;
293 }
294
302 private function getFooterIcons(): array {
303 $dataIcons = [];
304 $skinContext = $this->skinContext;
305 $config = $skinContext->getConfig();
306 // If footer icons are enabled append to the end of the rows
307 $footerIcons = self::getFooterIconsData(
308 $config
309 );
310
311 if ( count( $footerIcons ) > 0 ) {
312 $icons = [];
313 foreach ( $footerIcons as $blockName => $blockIcons ) {
314 $html = '';
315 foreach ( $blockIcons as $icon ) {
316 $html .= self::makeFooterIconHTML(
317 $config, $icon
318 );
319 }
320 // For historic reasons this mimics the `icononly` option
321 // for BaseTemplate::getFooterIcons. Empty rows should not be output.
322 if ( $html ) {
323 $block = htmlspecialchars( $blockName );
324 $icons[$block] = [
325 'name' => $block,
326 'id' => 'footer-' . $block . 'ico',
327 'html' => $html,
328 'class' => [ 'noprint' ],
329 ];
330 }
331 }
332
333 // Empty rows should not be output.
334 // This is how Vector has behaved historically but we can revisit later if necessary.
335 if ( count( $icons ) > 0 ) {
336 $dataIcons = new SkinComponentMenu(
337 'footer-icons',
338 $icons,
339 $this->skinContext->getMessageLocalizer(),
340 '',
341 []
342 );
343 }
344 }
345
346 return $dataIcons ? $dataIcons->getTemplateData() : [];
347 }
348
360 private function formatFooterDataForCurrentSpec( array $data ): array {
361 $formattedData = [];
362 foreach ( $data as $key => $item ) {
363 unset( $item['html-tooltip'] );
364 unset( $item['html-items'] );
365 unset( $item['html-after-portal'] );
366 unset( $item['html-before-portal'] );
367 unset( $item['label'] );
368 unset( $item['class'] );
369 foreach ( $item['array-items'] ?? [] as $index => $arrayItem ) {
370 unset( $item['array-items'][$index]['html-item'] );
371 }
372 $formattedData[$key] = $item;
373 $formattedData[$key]['className'] = $key === 'data-icons' ? 'noprint' : null;
374 }
375 return $formattedData;
376 }
377
384 private function lastModified() {
385 $skinContext = $this->skinContext;
386 $out = $skinContext->getOutput();
387 $timestamp = $out->getRevisionTimestamp();
388
389 // No cached timestamp, load it from the database
390 // TODO: This code shouldn't be necessary, revision ID should always be available
391 // Move this logic to OutputPage::getRevisionTimestamp if needed.
392 if ( $timestamp === null ) {
393 $revId = $out->getRevisionId();
394 if ( $revId !== null ) {
395 $timestamp = MediaWikiServices::getInstance()->getRevisionLookup()->getTimestampFromId( $revId );
396 }
397 }
398
399 $lastModified = new SkinComponentLastModified(
400 $skinContext,
401 $timestamp
402 );
403
404 return $lastModified->getTemplateData()['text'];
405 }
406
413 private function renderedWith() {
414 $skinContext = $this->skinContext;
415 $out = $skinContext->getOutput();
416 $useParsoid = $out->getOutputFlag( ParserOutputFlags::USE_PARSOID );
417
418 $renderedWith = new SkinComponentRenderedWith(
419 $skinContext,
420 $useParsoid
421 );
422
423 return $renderedWith->getTemplateData()['text'];
424 }
425}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:82
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:57
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:76
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...