Merge "Special:BlockList: Update remove/change block links"
[mediawiki.git] / includes / skins / components / SkinComponentFooter.php
blob93bfa73de398b47acfe9eef2bdfc1074fe236ce0
1 <?php
3 namespace MediaWiki\Skin;
5 use Action;
6 use Article;
7 use CreditsAction;
8 use MediaWiki\Config\Config;
9 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
10 use MediaWiki\Html\Html;
11 use MediaWiki\MainConfigNames;
12 use MediaWiki\MediaWikiServices;
13 use MediaWiki\Title\Title;
15 class SkinComponentFooter implements SkinComponent {
16 use ProtectedHookAccessorTrait;
18 /** @var SkinComponentRegistryContext */
19 private $skinContext;
21 public function __construct( SkinComponentRegistryContext $skinContext ) {
22 $this->skinContext = $skinContext;
25 /**
26 * Run SkinAddFooterLinks hook on menu data to insert additional menu items specifically in footer.
28 private function getTemplateDataFooter(): array {
29 $data = [
30 'info' => $this->formatFooterInfoData(
31 $this->getFooterInfoData()
33 'places' => $this->getSiteFooterLinks(),
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,
46 return $data;
49 /**
50 * @inheritDoc
52 public function getTemplateData(): array {
53 $footerData = $this->getTemplateDataFooter();
55 // Create the menu components from the footer data.
56 $footerInfoMenuData = new SkinComponentMenu(
57 'footer-info',
58 $footerData['info'],
59 $this->skinContext->getMessageLocalizer()
61 $footerSiteMenuData = new SkinComponentMenu(
62 'footer-places',
63 $footerData['places'],
64 $this->skinContext->getMessageLocalizer()
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 );
77 return [
78 'data-info' => $footerMenuData['data-info'],
79 'data-places' => $footerMenuData['data-places'],
80 'data-icons' => $footerMenuData['data-icons']
84 /**
85 * Get the footer data containing standard footer links.
87 * All values are resolved and can be added to by the
88 * SkinAddFooterLinks hook.
90 * @since 1.40
91 * @internal
92 * @return array
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;
110 /** @var CreditsAction $action */
111 if ( $useCredits ) {
112 $article = Article::newFromWikiPage( $skinContext->getWikiPage(), $ctx );
113 $action = Action::factory( 'credits', $article, $ctx );
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,
128 * @return string
130 private function getCopyright() {
131 $copyright = new SkinComponentCopyright( $this->skinContext );
132 return $copyright->getTemplateData()[ 'html' ];
136 * Format the footer data containing standard footer links for passing
137 * into SkinComponentMenu.
139 * @since 1.40
140 * @internal
141 * @param array $data raw footer data
142 * @return array
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
154 return $formattedData;
158 * Gets the link to the wiki's privacy policy, about page, and disclaimer page
160 * @internal
161 * @return array data array for 'privacy', 'about', 'disclaimer'
163 private function getSiteFooterLinks(): array {
164 $siteLinksData = [];
165 $siteLinks = [
166 'privacy' => [ 'privacy', 'privacypage' ],
167 'about' => [ 'aboutsite', 'aboutpage' ],
168 'disclaimers' => [ 'disclaimers', 'disclaimerpage' ]
170 $localizer = $this->skinContext->getMessageLocalizer();
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()
188 return $siteLinksData;
192 * Renders a $wgFooterIcons icon according to the method's arguments
194 * @param Config $config
195 * @param array|string $icon The icon to build the html for, see $wgFooterIcons
196 * for the format of this array.
197 * @param string $withImage Whether to use the icon's image or output
198 * a text-only footer icon.
199 * @return string HTML
200 * @internal for use in Skin only
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 if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
209 // Lazy-load footer icons, since they're not part of the printed view.
210 $icon['loading'] = 'lazy';
211 // do this the lazy way, just pass icon data as an attribute array
212 $html = Html::element( 'img', $icon );
213 } else {
214 $html = htmlspecialchars( $icon['alt'] ?? '' );
216 if ( $url ) {
217 $html = Html::rawElement(
218 'a',
220 'href' => $url,
221 // Using a fake Codex link button, as this is the long-expected UX; our apologies.
222 'class' => [
223 'cdx-button', 'cdx-button--fake-button',
224 'cdx-button--size-large', 'cdx-button--fake-button--enabled'
226 'target' => $config->get( MainConfigNames::ExternalLinkTarget ),
228 $html
232 return $html;
236 * Get data representation of icons
238 * @internal for use in Skin only
239 * @param Config $config
240 * @return array
242 public static function getFooterIconsData( Config $config ) {
243 $footericons = [];
244 foreach (
245 $config->get( MainConfigNames::FooterIcons ) as $footerIconsKey => &$footerIconsBlock
247 if ( count( $footerIconsBlock ) > 0 ) {
248 $footericons[$footerIconsKey] = [];
249 foreach ( $footerIconsBlock as &$footerIcon ) {
250 if ( isset( $footerIcon['src'] ) ) {
251 if ( !isset( $footerIcon['width'] ) ) {
252 $footerIcon['width'] = 88;
254 if ( !isset( $footerIcon['height'] ) ) {
255 $footerIcon['height'] = 31;
259 // Only output icons which have an image.
260 // For historic reasons this mimics the `icononly` option
261 // for BaseTemplate::getFooterIcons.
262 // In some cases the icon may be an empty array.
263 // Filter these out. (See T269776)
264 if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) {
265 $footericons[$footerIconsKey][] = $footerIcon;
269 // If no valid icons with images were added, unset the parent array
270 // Should also prevent empty arrays from when no copyright is set.
271 if ( !count( $footericons[$footerIconsKey] ) ) {
272 unset( $footericons[$footerIconsKey] );
276 return $footericons;
280 * Gets the link to the wiki's privacy policy, about page, and disclaimer page
282 * @internal
283 * @return array data array for 'privacy', 'about', 'disclaimer'
284 * @suppress SecurityCheck-DoubleEscaped
286 private function getFooterIcons(): array {
287 $dataIcons = [];
288 $skinContext = $this->skinContext;
289 $config = $skinContext->getConfig();
290 // If footer icons are enabled append to the end of the rows
291 $footerIcons = self::getFooterIconsData(
292 $config
295 if ( count( $footerIcons ) > 0 ) {
296 $icons = [];
297 foreach ( $footerIcons as $blockName => $blockIcons ) {
298 $html = '';
299 foreach ( $blockIcons as $icon ) {
300 $html .= self::makeFooterIconHTML(
301 $config, $icon
304 // For historic reasons this mimics the `icononly` option
305 // for BaseTemplate::getFooterIcons. Empty rows should not be output.
306 if ( $html ) {
307 $block = htmlspecialchars( $blockName );
308 $icons[$block] = [
309 'name' => $block,
310 'id' => 'footer-' . $block . 'ico',
311 'html' => $html,
312 'class' => [ 'noprint' ],
317 // Empty rows should not be output.
318 // This is how Vector has behaved historically but we can revisit later if necessary.
319 if ( count( $icons ) > 0 ) {
320 $dataIcons = new SkinComponentMenu(
321 'footer-icons',
322 $icons,
323 $this->skinContext->getMessageLocalizer(),
330 return $dataIcons ? $dataIcons->getTemplateData() : [];
334 * Get finalized footer menu data and reformat to fit current specification.
336 * See https://www.mediawiki.org/wiki/Manual:SkinMustache.php#DataFooter
337 * This method should be removed once the specification is updated and
338 * new data properties provided by the menu component are ok to output.
340 * @internal
341 * @param array $data
342 * @return array
344 private function formatFooterDataForCurrentSpec( array $data ): array {
345 $formattedData = [];
346 foreach ( $data as $key => $item ) {
347 unset( $item['html-tooltip'] );
348 unset( $item['html-items'] );
349 unset( $item['html-after-portal'] );
350 unset( $item['html-before-portal'] );
351 unset( $item['label'] );
352 unset( $item['class'] );
353 foreach ( $item['array-items'] ?? [] as $index => $arrayItem ) {
354 unset( $item['array-items'][$index]['html-item'] );
356 $formattedData[$key] = $item;
357 $formattedData[$key]['className'] = $key === 'data-icons' ? 'noprint' : null;
359 return $formattedData;
363 * Get the timestamp of the latest revision, formatted in user language
365 * @internal for use in Skin.php only
366 * @return string
368 private function lastModified() {
369 $skinContext = $this->skinContext;
370 $out = $skinContext->getOutput();
371 $timestamp = $out->getRevisionTimestamp();
373 // No cached timestamp, load it from the database
374 // TODO: This code shouldn't be necessary, revision ID should always be available
375 // Move this logic to OutputPage::getRevisionTimestamp if needed.
376 if ( $timestamp === null ) {
377 $revId = $out->getRevisionId();
378 if ( $revId !== null ) {
379 $timestamp = MediaWikiServices::getInstance()->getRevisionLookup()->getTimestampFromId( $revId );
383 $lastModified = new SkinComponentLastModified(
384 $skinContext,
385 $timestamp
388 return $lastModified->getTemplateData()['text'];