391 lines
12 KiB
PHP
391 lines
12 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Skin;
|
|
|
|
use Action;
|
|
use Article;
|
|
use CreditsAction;
|
|
use MediaWiki\Config\Config;
|
|
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Title\Title;
|
|
|
|
class SkinComponentFooter implements SkinComponent {
|
|
use ProtectedHookAccessorTrait;
|
|
|
|
/** @var SkinComponentRegistryContext */
|
|
private $skinContext;
|
|
|
|
/**
|
|
* @param SkinComponentRegistryContext $skinContext
|
|
*/
|
|
public function __construct( SkinComponentRegistryContext $skinContext ) {
|
|
$this->skinContext = $skinContext;
|
|
}
|
|
|
|
/**
|
|
* Run SkinAddFooterLinks hook on menu data to insert additional menu items specifically in footer.
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getTemplateDataFooter(): array {
|
|
$data = [
|
|
'info' => $this->formatFooterInfoData(
|
|
$this->getFooterInfoData()
|
|
),
|
|
'places' => $this->getSiteFooterLinks(),
|
|
];
|
|
$skin = $this->skinContext->getContextSource()->getSkin();
|
|
foreach ( $data as $key => $existingItems ) {
|
|
$newItems = [];
|
|
$this->getHookRunner()->onSkinAddFooterLinks( $skin, $key, $newItems );
|
|
foreach ( $newItems as $index => $linkHTML ) {
|
|
$data[ $key ][ $index ] = [
|
|
'id' => 'footer-' . $key . '-' . $index,
|
|
'html' => $linkHTML,
|
|
];
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function getTemplateData(): array {
|
|
$footerData = $this->getTemplateDataFooter();
|
|
|
|
// Create the menu components from the footer data.
|
|
$footerInfoMenuData = new SkinComponentMenu(
|
|
'footer-info',
|
|
$footerData['info'],
|
|
$this->skinContext->getMessageLocalizer()
|
|
);
|
|
$footerSiteMenuData = new SkinComponentMenu(
|
|
'footer-places',
|
|
$footerData['places'],
|
|
$this->skinContext->getMessageLocalizer()
|
|
);
|
|
|
|
// To conform the footer menu data to the current SkinMustache specification,
|
|
// run the derived data through a cleanup function to unset unexpected data properties
|
|
// until the spec is updated to reflect the new properties introduced by the menu component.
|
|
// See https://www.mediawiki.org/wiki/Manual:SkinMustache.php#DataFooter
|
|
$footerMenuData = [];
|
|
$footerMenuData['data-info'] = $footerInfoMenuData->getTemplateData();
|
|
$footerMenuData['data-places'] = $footerSiteMenuData->getTemplateData();
|
|
$footerMenuData['data-icons'] = $this->getFooterIcons();
|
|
$footerMenuData = $this->formatFooterDataForCurrentSpec( $footerMenuData );
|
|
|
|
return [
|
|
'data-info' => $footerMenuData['data-info'],
|
|
'data-places' => $footerMenuData['data-places'],
|
|
'data-icons' => $footerMenuData['data-icons']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the footer data containing standard footer links.
|
|
*
|
|
* All values are resolved and can be added to by the
|
|
* SkinAddFooterLinks hook.
|
|
*
|
|
* @since 1.40
|
|
* @internal
|
|
* @return array
|
|
*/
|
|
private function getFooterInfoData(): array {
|
|
$action = null;
|
|
$skinContext = $this->skinContext;
|
|
$out = $skinContext->getOutput();
|
|
$ctx = $skinContext->getContextSource();
|
|
// This needs to be the relevant Title rather than just the raw Title for e.g. special pages that render content
|
|
$title = $skinContext->getRelevantTitle();
|
|
$titleExists = $title && $title->exists();
|
|
$config = $skinContext->getConfig();
|
|
$maxCredits = $config->get( MainConfigNames::MaxCredits );
|
|
$showCreditsIfMax = $config->get( MainConfigNames::ShowCreditsIfMax );
|
|
$useCredits = $titleExists
|
|
&& $out->isArticle()
|
|
&& $out->isRevisionCurrent()
|
|
&& $maxCredits !== 0;
|
|
|
|
/** @var CreditsAction $action */
|
|
if ( $useCredits ) {
|
|
$article = Article::newFromWikiPage( $skinContext->getWikiPage(), $ctx );
|
|
$action = Action::factory( 'credits', $article, $ctx );
|
|
}
|
|
|
|
'@phan-var CreditsAction $action';
|
|
return [
|
|
'lastmod' => !$useCredits ? $this->lastModified() : null,
|
|
'numberofwatchingusers' => null,
|
|
'credits' => $useCredits && $action ?
|
|
$action->getCredits( $maxCredits, $showCreditsIfMax ) : null,
|
|
'copyright' => $titleExists &&
|
|
$out->showsCopyright() ? $this->getCopyright() : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
private function getCopyright() {
|
|
$copyright = new SkinComponentCopyright( $this->skinContext );
|
|
return $copyright->getTemplateData()[ 'html' ];
|
|
}
|
|
|
|
/**
|
|
* Format the footer data containing standard footer links for passing
|
|
* into SkinComponentMenu.
|
|
*
|
|
* @since 1.40
|
|
* @internal
|
|
* @param array $data raw footer data
|
|
* @return array
|
|
*/
|
|
private function formatFooterInfoData( array $data ): array {
|
|
$formattedData = [];
|
|
foreach ( $data as $key => $item ) {
|
|
if ( $item ) {
|
|
$formattedData[ $key ] = [
|
|
'id' => 'footer-info-' . $key,
|
|
'html' => $item
|
|
];
|
|
}
|
|
}
|
|
return $formattedData;
|
|
}
|
|
|
|
/**
|
|
* Gets the link to the wiki's privacy policy, about page, and disclaimer page
|
|
*
|
|
* @internal
|
|
* @return array data array for 'privacy', 'about', 'disclaimer'
|
|
*/
|
|
private function getSiteFooterLinks(): array {
|
|
$siteLinksData = [];
|
|
$siteLinks = [
|
|
'privacy' => [ 'privacy', 'privacypage' ],
|
|
'about' => [ 'aboutsite', 'aboutpage' ],
|
|
'disclaimers' => [ 'disclaimers', 'disclaimerpage' ]
|
|
];
|
|
$localizer = $this->skinContext->getMessageLocalizer();
|
|
|
|
foreach ( $siteLinks as $key => $siteLink ) {
|
|
// Check if the link description has been disabled in the default language.
|
|
// If disabled, it is disabled for all languages.
|
|
if ( !$localizer->msg( $siteLink[0] )->inContentLanguage()->isDisabled() ) {
|
|
// Display the link for the user, described in their language (which may or may not be the same as the
|
|
// default language), but make the link target be the one site-wide page.
|
|
$title = Title::newFromText( $localizer->msg( $siteLink[1] )->inContentLanguage()->text() );
|
|
if ( $title !== null ) {
|
|
$siteLinksData[$key] = [
|
|
'id' => "footer-places-$key",
|
|
'text' => $localizer->msg( $siteLink[0] )->text(),
|
|
'href' => $title->fixSpecialName()->getLinkURL()
|
|
];
|
|
}
|
|
}
|
|
}
|
|
return $siteLinksData;
|
|
}
|
|
|
|
/**
|
|
* Renders a $wgFooterIcons icon according to the method's arguments
|
|
*
|
|
* @param Config $config
|
|
* @param array|string $icon The icon to build the html for, see $wgFooterIcons
|
|
* for the format of this array.
|
|
* @param string $withImage Whether to use the icon's image or output
|
|
* a text-only footer icon.
|
|
* @return string HTML
|
|
* @internal for use in Skin only
|
|
*/
|
|
public static function makeFooterIconHTML( Config $config, $icon, string $withImage = 'withImage' ): string {
|
|
if ( is_string( $icon ) ) {
|
|
$html = $icon;
|
|
} else { // Assuming array
|
|
$url = $icon['url'] ?? null;
|
|
unset( $icon['url'] );
|
|
if ( isset( $icon['src'] ) && $withImage === 'withImage' ) {
|
|
// Lazy-load footer icons, since they're not part of the printed view.
|
|
$icon['loading'] = 'lazy';
|
|
// do this the lazy way, just pass icon data as an attribute array
|
|
$html = Html::element( 'img', $icon );
|
|
} else {
|
|
$html = htmlspecialchars( $icon['alt'] ?? '' );
|
|
}
|
|
if ( $url ) {
|
|
$html = Html::rawElement(
|
|
'a',
|
|
[
|
|
'href' => $url,
|
|
// Using a fake Codex link button, as this is the long-expected UX; our apologies.
|
|
'class' => [
|
|
'cdx-button', 'cdx-button--fake-button',
|
|
'cdx-button--size-large', 'cdx-button--fake-button--enabled'
|
|
],
|
|
'target' => $config->get( MainConfigNames::ExternalLinkTarget ),
|
|
],
|
|
$html
|
|
);
|
|
}
|
|
}
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Get data representation of icons
|
|
*
|
|
* @internal for use in Skin only
|
|
* @param Config $config
|
|
* @return array
|
|
*/
|
|
public static function getFooterIconsData( Config $config ) {
|
|
$footericons = [];
|
|
foreach (
|
|
$config->get( MainConfigNames::FooterIcons ) as $footerIconsKey => &$footerIconsBlock
|
|
) {
|
|
if ( count( $footerIconsBlock ) > 0 ) {
|
|
$footericons[$footerIconsKey] = [];
|
|
foreach ( $footerIconsBlock as &$footerIcon ) {
|
|
if ( isset( $footerIcon['src'] ) ) {
|
|
if ( !isset( $footerIcon['width'] ) ) {
|
|
$footerIcon['width'] = 88;
|
|
}
|
|
if ( !isset( $footerIcon['height'] ) ) {
|
|
$footerIcon['height'] = 31;
|
|
}
|
|
}
|
|
|
|
// Only output icons which have an image.
|
|
// For historic reasons this mimics the `icononly` option
|
|
// for BaseTemplate::getFooterIcons.
|
|
// In some cases the icon may be an empty array.
|
|
// Filter these out. (See T269776)
|
|
if ( is_string( $footerIcon ) || isset( $footerIcon['src'] ) ) {
|
|
$footericons[$footerIconsKey][] = $footerIcon;
|
|
}
|
|
}
|
|
|
|
// If no valid icons with images were added, unset the parent array
|
|
// Should also prevent empty arrays from when no copyright is set.
|
|
if ( !count( $footericons[$footerIconsKey] ) ) {
|
|
unset( $footericons[$footerIconsKey] );
|
|
}
|
|
}
|
|
}
|
|
return $footericons;
|
|
}
|
|
|
|
/**
|
|
* Gets the link to the wiki's privacy policy, about page, and disclaimer page
|
|
*
|
|
* @internal
|
|
* @return array data array for 'privacy', 'about', 'disclaimer'
|
|
* @suppress SecurityCheck-DoubleEscaped
|
|
*/
|
|
private function getFooterIcons(): array {
|
|
$dataIcons = [];
|
|
$skinContext = $this->skinContext;
|
|
// If footer icons are enabled append to the end of the rows
|
|
$footerIcons = $skinContext->getFooterIcons();
|
|
|
|
if ( count( $footerIcons ) > 0 ) {
|
|
$icons = [];
|
|
foreach ( $footerIcons as $blockName => $blockIcons ) {
|
|
$html = '';
|
|
foreach ( $blockIcons as $icon ) {
|
|
$html .= $skinContext->makeFooterIcon( $icon );
|
|
}
|
|
// For historic reasons this mimics the `icononly` option
|
|
// for BaseTemplate::getFooterIcons. Empty rows should not be output.
|
|
if ( $html ) {
|
|
$block = htmlspecialchars( $blockName );
|
|
$icons[$block] = [
|
|
'name' => $block,
|
|
'id' => 'footer-' . $block . 'ico',
|
|
'html' => $html,
|
|
'class' => [ 'noprint' ],
|
|
];
|
|
}
|
|
}
|
|
|
|
// Empty rows should not be output.
|
|
// This is how Vector has behaved historically but we can revisit later if necessary.
|
|
if ( count( $icons ) > 0 ) {
|
|
$dataIcons = new SkinComponentMenu(
|
|
'footer-icons',
|
|
$icons,
|
|
$this->skinContext->getMessageLocalizer(),
|
|
'',
|
|
[]
|
|
);
|
|
}
|
|
}
|
|
|
|
return $dataIcons ? $dataIcons->getTemplateData() : [];
|
|
}
|
|
|
|
/**
|
|
* Get finalized footer menu data and reformat to fit current specification.
|
|
*
|
|
* See https://www.mediawiki.org/wiki/Manual:SkinMustache.php#DataFooter
|
|
* This method should be removed once the specification is updated and
|
|
* new data properties provided by the menu component are ok to output.
|
|
*
|
|
* @internal
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
private function formatFooterDataForCurrentSpec( array $data ): array {
|
|
$formattedData = [];
|
|
foreach ( $data as $key => $item ) {
|
|
unset( $item['html-tooltip'] );
|
|
unset( $item['html-items'] );
|
|
unset( $item['html-after-portal'] );
|
|
unset( $item['html-before-portal'] );
|
|
unset( $item['label'] );
|
|
unset( $item['class'] );
|
|
foreach ( $item['array-items'] ?? [] as $index => $arrayItem ) {
|
|
unset( $item['array-items'][$index]['html-item'] );
|
|
}
|
|
$formattedData[$key] = $item;
|
|
$formattedData[$key]['className'] = $key === 'data-icons' ? 'noprint' : null;
|
|
}
|
|
return $formattedData;
|
|
}
|
|
|
|
/**
|
|
* Get the timestamp of the latest revision, formatted in user language
|
|
*
|
|
* @internal for use in Skin.php only
|
|
* @return string
|
|
*/
|
|
private function lastModified() {
|
|
$skinContext = $this->skinContext;
|
|
$out = $skinContext->getOutput();
|
|
$timestamp = $out->getRevisionTimestamp();
|
|
|
|
// No cached timestamp, load it from the database
|
|
// TODO: This code shouldn't be necessary, revision ID should always be available
|
|
// Move this logic to OutputPage::getRevisionTimestamp if needed.
|
|
if ( $timestamp === null ) {
|
|
$revId = $out->getRevisionId();
|
|
if ( $revId !== null ) {
|
|
$timestamp = MediaWikiServices::getInstance()->getRevisionLookup()->getTimestampFromId( $revId );
|
|
}
|
|
}
|
|
|
|
$lastModified = new SkinComponentLastModified(
|
|
$skinContext,
|
|
$timestamp
|
|
);
|
|
|
|
return $lastModified->getTemplateData()['text'];
|
|
}
|
|
}
|