MediaWiki master
SkinComponentFooter.php
Go to the documentation of this file.
1<?php
2
4
8use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
15
17 use ProtectedHookAccessorTrait;
18
20 private $skinContext;
21
23 private $cachedTemplateData;
24
25 public function __construct( SkinComponentRegistryContext $skinContext ) {
26 $this->skinContext = $skinContext;
27 }
28
32 private function getTemplateDataFooter(): array {
33 $data = [
34 'info' => $this->formatFooterInfoData(
35 $this->getFooterInfoData()
36 ),
37 'places' => $this->getSiteFooterLinks(),
38 ];
39 $skin = $this->skinContext->getContextSource()->getSkin();
40 foreach ( $data as $key => $existingItems ) {
41 $newItems = [];
42 $this->getHookRunner()->onSkinAddFooterLinks( $skin, $key, $newItems );
43 foreach ( $newItems as $index => $linkHTML ) {
44 $data[ $key ][ $index ] = [
45 'id' => 'footer-' . $key . '-' . $index,
46 'html' => $linkHTML,
47 ];
48 }
49 }
50
51 return $data;
52 }
53
57 public function getTemplateData(): array {
58 if ( $this->cachedTemplateData ) {
59 return $this->cachedTemplateData;
60 }
61
62 $footerData = $this->getTemplateDataFooter();
63
64 // Create the menu components from the footer data.
65 $footerInfoMenuData = new SkinComponentMenu(
66 'footer-info',
67 $footerData['info'],
68 $this->skinContext->getMessageLocalizer()
69 );
70 $footerSiteMenuData = new SkinComponentMenu(
71 'footer-places',
72 $footerData['places'],
73 $this->skinContext->getMessageLocalizer()
74 );
75
76 // To conform the footer menu data to the current SkinMustache specification,
77 // run the derived data through a cleanup function to unset unexpected data properties
78 // until the spec is updated to reflect the new properties introduced by the menu component.
79 // See https://www.mediawiki.org/wiki/Manual:SkinMustache.php#DataFooter
80 $footerMenuData = $this->formatFooterDataForCurrentSpec( [
81 'data-info' => $footerInfoMenuData->getTemplateData(),
82 'data-places' => $footerSiteMenuData->getTemplateData(),
83 'data-icons' => $this->getFooterIcons(),
84 ] );
85
86 $this->cachedTemplateData = [
87 'data-info' => $footerMenuData['data-info'],
88 'data-places' => $footerMenuData['data-places'],
89 'data-icons' => $footerMenuData['data-icons']
90 ];
91 return $this->cachedTemplateData;
92 }
93
104 private function getFooterInfoData(): array {
105 $action = null;
106 $skinContext = $this->skinContext;
107 $out = $skinContext->getOutput();
108 $ctx = $skinContext->getContextSource();
109 // This needs to be the relevant Title rather than just the raw Title for e.g. special pages that render content
110 $title = $skinContext->getRelevantTitle();
111 $titleExists = $title && $title->exists();
112 $config = $skinContext->getConfig();
113 $maxCredits = $config->get( MainConfigNames::MaxCredits );
114 $showCreditsIfMax = $config->get( MainConfigNames::ShowCreditsIfMax );
115 $useCredits = $titleExists
116 && $out->isArticle()
117 && $out->isRevisionCurrent()
118 && $maxCredits !== 0;
119
121 if ( $useCredits ) {
122 $article = Article::newFromWikiPage( $skinContext->getWikiPage(), $ctx );
123 $action = Action::factory( 'credits', $article, $ctx );
124 }
125
126 '@phan-var CreditsAction $action';
127 return [
128 'lastmod' => !$useCredits ? $this->lastModified() : null,
129 'numberofwatchingusers' => null,
130 'credits' => $useCredits && $action ?
131 $action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
132 'renderedwith' => $this->renderedWith(),
133 // Copyright is often rendered as a block in skins and
134 // should thus be last, after the inline elements.
135 'copyright' => $titleExists &&
136 $out->showsCopyright() ? $this->getCopyright() : null,
137 ];
138 }
139
143 private function getCopyright() {
144 $copyright = new SkinComponentCopyright( $this->skinContext );
145 return $copyright->getTemplateData()[ 'html' ];
146 }
147
157 private function formatFooterInfoData( array $data ): array {
158 $formattedData = [];
159 foreach ( $data as $key => $item ) {
160 if ( $item ) {
161 $formattedData[ $key ] = [
162 'id' => 'footer-info-' . $key,
163 'html' => $item
164 ];
165 }
166 }
167 return $formattedData;
168 }
169
176 private function getSiteFooterLinks(): array {
177 $siteLinksData = [];
178 $siteLinks = [
179 'privacy' => [ 'privacy', 'privacypage' ],
180 'about' => [ 'aboutsite', 'aboutpage' ],
181 'disclaimers' => [ 'disclaimers', 'disclaimerpage' ]
182 ];
183 $localizer = $this->skinContext->getMessageLocalizer();
184
185 foreach ( $siteLinks as $key => $siteLink ) {
186 // Check if the link description has been disabled in the default language.
187 // If disabled, it is disabled for all languages.
188 if ( !$localizer->msg( $siteLink[0] )->inContentLanguage()->isDisabled() ) {
189 // Display the link for the user, described in their language (which may or may not be the same as the
190 // default language), but make the link target be the one site-wide page.
191 $title = Title::newFromText( $localizer->msg( $siteLink[1] )->inContentLanguage()->text() );
192 if ( $title !== null ) {
193 $siteLinksData[$key] = [
194 'id' => "footer-places-$key",
195 'text' => $localizer->msg( $siteLink[0] )->text(),
196 'href' => $title->fixSpecialName()->getLinkURL()
197 ];
198 }
199 }
200 }
201 return $siteLinksData;
202 }
203
215 public static function makeFooterIconHTML( Config $config, $icon, string $withImage = 'withImage' ): string {
216 if ( is_string( $icon ) ) {
217 $html = $icon;
218 } else { // Assuming array
219 $url = $icon['url'] ?? null;
220 unset( $icon['url'] );
221
222 $sources = '';
223 if ( isset( $icon['sources'] ) ) {
224 foreach ( $icon['sources'] as $source ) {
225 $sources .= Html::element( 'source', $source );
226 }
227 unset( $icon['sources'] );
228 }
229
230 if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
231 // Lazy-load footer icons, since they're not part of the printed view.
232 $icon['loading'] = 'lazy';
233 // do this the lazy way, just pass icon data as an attribute array
234 $html = Html::element( 'img', $icon );
235 if ( $sources ) {
236 $html = Html::rawElement( 'picture', [], $sources . $html );
237 }
238 } else {
239 $html = htmlspecialchars( $icon['alt'] ?? '' );
240 }
241 if ( $url ) {
242 $html = Html::rawElement(
243 'a',
244 [
245 'href' => $url,
246 // Using a fake Codex link button, as this is the long-expected UX; our apologies.
247 'class' => [
248 'cdx-button', 'cdx-button--fake-button',
249 'cdx-button--size-large', 'cdx-button--fake-button--enabled'
250 ],
251 'target' => $config->get( MainConfigNames::ExternalLinkTarget ),
252 ],
253 $html
254 );
255 }
256 }
257 return $html;
258 }
259
267 public static function getFooterIconsData( Config $config ) {
268 $footericons = [];
269 foreach (
270 $config->get( MainConfigNames::FooterIcons ) as $footerIconsKey => &$footerIconsBlock
271 ) {
272 if ( count( $footerIconsBlock ) > 0 ) {
273 $footericons[$footerIconsKey] = [];
274 foreach ( $footerIconsBlock as &$footerIcon ) {
275 if ( isset( $footerIcon['src'] ) ) {
276 if ( !isset( $footerIcon['width'] ) ) {
277 $footerIcon['width'] = 88;
278 }
279 if ( !isset( $footerIcon['height'] ) ) {
280 $footerIcon['height'] = 31;
281 }
282 }
283
284 // Only output icons which have an image.
285 // For historic reasons this mimics the `icononly` option
286 // for BaseTemplate::getFooterIcons.
287 // In some cases the icon may be an empty array.
288 // Filter these out. (See T269776)
289 if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) {
290 $footericons[$footerIconsKey][] = $footerIcon;
291 }
292 }
293
294 // If no valid icons with images were added, unset the parent array
295 // Should also prevent empty arrays from when no copyright is set.
296 if ( !count( $footericons[$footerIconsKey] ) ) {
297 unset( $footericons[$footerIconsKey] );
298 }
299 }
300 }
301 return $footericons;
302 }
303
311 private function getFooterIcons(): array {
312 $dataIcons = [];
313 $skinContext = $this->skinContext;
314 $config = $skinContext->getConfig();
315 // If footer icons are enabled append to the end of the rows
316 $footerIcons = self::getFooterIconsData(
317 $config
318 );
319
320 if ( count( $footerIcons ) > 0 ) {
321 $icons = [];
322 foreach ( $footerIcons as $blockName => $blockIcons ) {
323 $html = '';
324 foreach ( $blockIcons as $icon ) {
325 $html .= self::makeFooterIconHTML(
326 $config, $icon
327 );
328 }
329 // For historic reasons this mimics the `icononly` option
330 // for BaseTemplate::getFooterIcons. Empty rows should not be output.
331 if ( $html ) {
332 $block = htmlspecialchars( $blockName );
333 $icons[$block] = [
334 'name' => $block,
335 'id' => 'footer-' . $block . 'ico',
336 'html' => $html,
337 'class' => [ 'noprint' ],
338 ];
339 }
340 }
341
342 // Empty rows should not be output.
343 // This is how Vector has behaved historically but we can revisit later if necessary.
344 if ( count( $icons ) > 0 ) {
345 $dataIcons = new SkinComponentMenu(
346 'footer-icons',
347 $icons,
348 $this->skinContext->getMessageLocalizer(),
349 '',
350 []
351 );
352 }
353 }
354
355 return $dataIcons ? $dataIcons->getTemplateData() : [];
356 }
357
369 private function formatFooterDataForCurrentSpec( array $data ): array {
370 $formattedData = [];
371 foreach ( $data as $key => $item ) {
372 unset( $item['html-tooltip'] );
373 unset( $item['html-items'] );
374 unset( $item['html-after-portal'] );
375 unset( $item['html-before-portal'] );
376 unset( $item['label'] );
377 unset( $item['class'] );
378 foreach ( $item['array-items'] ?? [] as $index => $arrayItem ) {
379 unset( $item['array-items'][$index]['html-item'] );
380 }
381 $formattedData[$key] = $item;
382 $formattedData[$key]['className'] = $key === 'data-icons' ? 'noprint' : null;
383 }
384 return $formattedData;
385 }
386
393 private function lastModified() {
394 $skinContext = $this->skinContext;
395 $out = $skinContext->getOutput();
396 $timestamp = $out->getRevisionTimestamp();
397
398 // No cached timestamp, load it from the database
399 // TODO: This code shouldn't be necessary, revision ID should always be available
400 // Move this logic to OutputPage::getRevisionTimestamp if needed.
401 if ( $timestamp === null ) {
402 $revId = $out->getRevisionId();
403 if ( $revId !== null ) {
404 $timestamp = MediaWikiServices::getInstance()->getRevisionLookup()->getTimestampFromId( $revId );
405 }
406 }
407
408 $lastModified = new SkinComponentLastModified(
409 $skinContext,
410 $timestamp
411 );
412
413 return $lastModified->getTemplateData()['text'];
414 }
415
422 private function renderedWith() {
423 $skinContext = $this->skinContext;
424 $out = $skinContext->getOutput();
425 $useParsoid = $out->getOutputFlag( ParserOutputFlags::USE_PARSOID );
426
427 $renderedWith = new SkinComponentRenderedWith(
428 $this->skinContext->getMessageLocalizer(),
429 $useParsoid
430 );
431
432 return $renderedWith->getTemplateData()['text'];
433 }
434}
435
437class_alias( SkinComponentFooter::class, 'MediaWiki\\Skin\\SkinComponentFooter' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Actions are things which can be done to pages (edit, delete, rollback, etc).
Definition Action.php:53
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
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:66
__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.
getTemplateData()
This returns all the data that is needed to the component.Returned array must be serialized....
getConfig()
Returns the config needed for the component.Config
Represents a title within MediaWiki.
Definition Title.php:69
Interface for configuration instances.
Definition Config.php:18
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$source