mediawiki-skins-Vector/includes/Components/VectorComponentUserLinks.php
Ammarpad 02dfbd4acb Do not show empty user links dropdown in private wikis
In situations where anon account creation is disabled or when
combined login link would be used, the usermenu array contains
single element (for 'login' or 'login/create') instead of two
elements ('login' and 'create account' items separately)

If $wgWikimediaMessagesAnonDonateLink is enabled through the
WikimediaMessages extension, the third element 'sitesupport'
exists, making the number of anon items either 1 or 2 or 3.

This patch fixes $isDefaultAnonUserLinks variable to account
for all the cases

Additionally, if even login is disabled, the element count can be
zero and in such case we need to hide the dropdown in all cases
(not only on large viewports) because it would always be empty.

This patch passes the userlinks count to ::getDropdown() and adds
CSS class when appropriate to hide the drowpdown completely.

Bug: T332743
Change-Id: I1ce5e1ea30917a6e80ef00f3c1703cbd0ecb6968
2024-11-04 11:35:23 +01:00

355 lines
12 KiB
PHP

<?php
namespace MediaWiki\Skins\Vector\Components;
use MediaWiki\Linker\Linker;
use MediaWiki\Message\Message;
use MediaWiki\Skin\SkinComponentLink;
use MediaWiki\Title\MalformedTitleException;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use MessageLocalizer;
/**
* VectorComponentUserLinks component
*/
class VectorComponentUserLinks implements VectorComponent {
private const BUTTON_CLASSES = 'cdx-button cdx-button--fake-button '
. 'cdx-button--fake-button--enabled cdx-button--weight-quiet';
private const ICON_ONLY_BUTTON_CLASS = 'cdx-button--icon-only';
/** @var MessageLocalizer */
private $localizer;
/** @var UserIdentity */
private $user;
/** @var array */
private $portletData;
/** @var array */
private $linkOptions;
/** @var string */
private $userIcon;
/**
* @param MessageLocalizer $localizer
* @param UserIdentity $user
* @param array $portletData
* @param array $linkOptions
* @param string $userIcon that represents the current type of user
*/
public function __construct(
MessageLocalizer $localizer,
UserIdentity $user,
array $portletData,
array $linkOptions,
string $userIcon = 'userAvatar'
) {
$this->localizer = $localizer;
$this->user = $user;
$this->portletData = $portletData;
$this->linkOptions = $linkOptions;
$this->userIcon = $userIcon;
}
/**
* @param string $key
* @return Message
*/
private function msg( $key ): Message {
return $this->localizer->msg( $key );
}
/**
* @param bool $isDefaultAnonUserLinks
* @param bool $isAnonEditorLinksEnabled
* @param int $userLinksCount
* @return VectorComponentDropdown
*/
private function getDropdown( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled, $userLinksCount ) {
$user = $this->user;
$isAnon = !$user->isRegistered();
$class = 'vector-user-menu';
$class .= ' vector-button-flush-right';
$class .= !$isAnon ?
' vector-user-menu-logged-in' :
' vector-user-menu-logged-out';
// Hide entire user links dropdown on larger viewports if it only contains
// create account & login link, which are only shown on smaller viewports
if ( $isAnon && $isDefaultAnonUserLinks && !$isAnonEditorLinksEnabled ) {
$linkclass = ' user-links-collapsible-item';
if ( $userLinksCount === 0 ) {
// The user links can be completely empty when even login is not possible
// (e.g using remote authentication). In this case, we need to hide the
// dropdown completely not only on larger viewports.
$linkclass .= '--none';
}
$class .= $linkclass;
}
$tooltip = '';
$icon = $this->userIcon;
if ( $icon === '' && $userLinksCount ) {
$icon = 'ellipsis';
// T287494 We use tooltip messages to provide title attributes on hover over certain menu icons.
// For modern Vector, the "tooltip-p-personal" key is set to "User menu" which is appropriate for
// the user icon (dropdown indicator for user links menu) for logged-in users.
// This overrides the tooltip for the user links menu icon which is an ellipsis for anonymous users.
$tooltip = Linker::tooltip( 'vector-anon-user-menu-title' ) ?? '';
}
return new VectorComponentDropdown(
'vector-user-links-dropdown', $this->msg( 'personaltools' )->text(), $class, $icon, $tooltip
);
}
/**
* @param bool $isDefaultAnonUserLinks
* @param bool $isAnonEditorLinksEnabled
* @return array
*/
private function getMenus( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) {
$user = $this->user;
$isAnon = !$user->isRegistered();
$portletData = $this->portletData;
// Hide default user menu on larger viewports if it only contains
// create account & login link, which are only shown on smaller viewports
// FIXME: Replace array_merge with an add class helper function
$userMenuClass = $portletData[ 'data-user-menu' ][ 'class' ];
$userMenuClass = $isAnon && $isDefaultAnonUserLinks ?
$userMenuClass . ' user-links-collapsible-item' : $userMenuClass;
$dropdownMenus = [
new VectorComponentMenu( [
'label' => null,
'class' => $userMenuClass
] + $portletData[ 'data-user-menu' ] )
];
if ( $isAnon ) {
// T317789: The `anontalk` and `anoncontribs` links will not be added to
// the menu if `$wgGroupPermissions['*']['edit']` === false which can
// leave the menu empty due to our removal of other user menu items in
// `Hooks::updateUserLinksDropdownItems`. In this case, we do not want
// to render the anon "learn more" link.
if ( $isAnonEditorLinksEnabled ) {
$anonUserMenuData = $portletData[ 'data-user-menu-anon-editor' ];
try {
$anonEditorLabelLinkData = [
'text' => $this->msg( 'vector-anon-user-menu-pages-learn' )->text(),
'href' => Title::newFromTextThrow( $this->msg( 'vector-intro-page' )->text() )->getLocalURL(),
'aria-label' => $this->msg( 'vector-anon-user-menu-pages-label' )->text(),
];
$anonEditorLabelLink = new SkinComponentLink(
'', $anonEditorLabelLinkData, $this->localizer, $this->linkOptions
);
$anonEditorLabelLinkHtml = $anonEditorLabelLink->getTemplateData()[ 'html' ];
$anonUserMenuData['html-label'] = $this->msg( 'vector-anon-user-menu-pages' )->escaped() .
" " . $anonEditorLabelLinkHtml;
$anonUserMenuData['label'] = null;
} catch ( MalformedTitleException $e ) {
// ignore (T340220)
}
$dropdownMenus[] = new VectorComponentMenu( $anonUserMenuData );
}
} else {
// Logout isn't enabled for temp users, who are considered still considered registered
$isLogoutLinkEnabled = isset( $portletData[ 'data-user-menu-logout' ][ 'is-empty' ] ) &&
!$portletData[ 'data-user-menu-logout'][ 'is-empty' ];
if ( $isLogoutLinkEnabled ) {
$dropdownMenus[] = new VectorComponentMenu( [
'label' => null
] + $portletData[ 'data-user-menu-logout' ] );
}
}
return $dropdownMenus;
}
/**
* Strips icons from the menu.
*
* @param array $arrayListItems
* @return array
*/
private static function stripIcons( array $arrayListItems ) {
return array_map( static function ( $item ) {
$item['array-links'] = array_map( static function ( $link ) {
$link['icon'] = null;
return $link;
}, $item['array-links'] );
return $item;
}, $arrayListItems );
}
/**
* Converts links to button icons
*
* @param array $arrayListItems
* @param bool $iconOnlyButton whether label should be visible.
* @param array $exceptions list of names of items that should not be converted.
* @return array
*/
private static function makeLinksButtons( $arrayListItems, $iconOnlyButton = true, $exceptions = [] ) {
return array_map( static function ( $item ) use ( $iconOnlyButton, $exceptions ) {
if ( in_array( $item[ 'name'], $exceptions ) ) {
return $item;
}
$item['array-links'] = array_map( static function ( $link ) use ( $iconOnlyButton ) {
$link['array-attributes'] = array_map( static function ( $attribute ) use ( $iconOnlyButton ) {
if ( $attribute['key'] === 'class' ) {
$newClass = $attribute['value'] . ' ' . self::BUTTON_CLASSES;
if ( $iconOnlyButton ) {
$newClass .= ' ' . self::ICON_ONLY_BUTTON_CLASS;
}
$attribute['value'] = $newClass;
}
return $attribute;
}, $link['array-attributes'] );
return $link;
}, $item['array-links'] );
return $item;
}, $arrayListItems );
}
/**
* Makes all menu items collapsible at lower resolutions.
*
* @param array $arrayListItems
* @return array
*/
private static function makeItemsCollapsible( $arrayListItems ) {
return array_map( static function ( $item ) {
$item['class'] .= ' user-links-collapsible-item';
return $item;
}, $arrayListItems );
}
/**
* What class should the overflow menu have?
*
* @param array $arrayListItems
* @return string
*/
private static function getOverflowMenuClass( $arrayListItems ) {
$overflowMenuClass = 'mw-portlet';
if ( count( $arrayListItems ) === 0 ) {
$overflowMenuClass .= ' emptyPortlet';
}
return $overflowMenuClass;
}
/**
* @inheritDoc
*/
public function getTemplateData(): array {
$portletData = $this->portletData;
$userLinksCount = count( $portletData['data-user-menu']['array-items'] );
$isDefaultAnonUserLinks = $userLinksCount <= 3;
$isAnonEditorLinksEnabled = isset( $portletData['data-user-menu-anon-editor']['is-empty'] )
&& !$portletData['data-user-menu-anon-editor']['is-empty'];
$userInterfacePreferences = $this->makeLinksButtons(
$this->makeItemsCollapsible(
$portletData[ 'data-user-interface-preferences' ]['array-items'] ?? []
),
false
);
$userPage = $this->makeItemsCollapsible(
$this->stripIcons( $portletData[ 'data-user-page' ]['array-items'] ?? [] )
);
$notifications = $this->makeLinksButtons(
$portletData[ 'data-notifications' ]['array-items'] ?? [],
true,
[ 'talk-alert' ]
);
$overflow = $this->makeItemsCollapsible(
array_map(
static function ( $item ) {
// Since we're creating duplicate icons
$item['id'] .= '-2';
// Restore icon removed in hooks.
if ( $item['name'] === 'watchlist' ) {
$item['icon'] = 'watchlist';
}
return $item;
},
// array_filter preserves keys so use array_values to restore array.
array_values(
array_filter(
$portletData['data-user-menu']['array-items'] ?? [],
static function ( $item ) {
// Only certain items get promoted to the overflow menu:
// * watchlist
// * login
// * create account
$name = $item['name'];
return in_array( $name, [
'watchlist',
'createaccount',
'login',
'login-private',
'sitesupport'
] );
}
)
)
)
);
// Convert to buttons for logged in users.
// For anons these will remain as links.
// Note: This list is empty for temporary users currently.
if ( $this->user->isRegistered() ) {
$overflow = $this->makeLinksButtons( $overflow );
}
$preferencesMenu = new VectorComponentMenu( [
'id' => 'p-vector-user-menu-preferences',
'class' => self::getOverflowMenuClass( $userInterfacePreferences ),
'label' => null,
'html-items' => null,
'array-list-items' => $userInterfacePreferences,
] );
$userPageMenu = new VectorComponentMenu( [
'id' => 'p-vector-user-menu-userpage',
'class' => self::getOverflowMenuClass( $userPage ),
'label' => null,
'html-items' => null,
'array-list-items' => $userPage,
] );
$notificationsMenu = new VectorComponentMenu( [
'id' => 'p-vector-user-menu-notifications',
'class' => self::getOverflowMenuClass( $notifications ),
'label' => null,
'html-items' => null,
'array-list-items' => $notifications,
] );
$overflowMenu = new VectorComponentMenu( [
'id' => 'p-vector-user-menu-overflow',
'class' => self::getOverflowMenuClass( $overflow ),
'label' => null,
'html-items' => null,
'array-list-items' => $overflow,
] );
return [
'is-wide' => array_filter(
[ $overflow, $notifications, $userPage, $userInterfacePreferences ]
) !== [],
'data-user-links-notifications' => $notificationsMenu->getTemplateData(),
'data-user-links-overflow' => $overflowMenu->getTemplateData(),
'data-user-links-preferences' => $preferencesMenu->getTemplateData(),
'data-user-links-user-page' => $userPageMenu->getTemplateData(),
'data-user-links-dropdown' => $this->getDropdown(
$isDefaultAnonUserLinks, $isAnonEditorLinksEnabled, $userLinksCount )->getTemplateData(),
'data-user-links-menus' => array_map( static function ( $menu ) {
return $menu->getTemplateData();
}, $this->getMenus( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) ),
];
}
}