mediawiki-skins-MinervaNeue/includes/Menu/PageActions/ToolbarBuilder.php
Martin Urbanec 0109b17483 watchlist: Don't assume only named users have watchlist access
In I1ccda3878a658d4251429ae65ebee04c09d81243, core decided
to technically allow temp. users to have watchlist access,
but have it disabled on permission level
(viewmywatchlist / editmywatchlist) instead of by isNamed() checks.

Minerva expects the watchlist permissions to be assigned to everyone,
but actual watchlist access to be available only to named user.
This is an incorrect assumption. Make a clear difference between
"page is watchable" concept (watch icon should show) and "user has
permission to watch" (if not, CTA to log in should show).

There is one additional step to fix, which is ensure
temp users receive the T330518 drawer. That is not a bug
in MinervaNeue, but in core -- mediawiki.page.watch.ajax.js uses
e.stopPropagating(), which means Minerva's CtaDrawer doesn't get
a say. Ideally, mediawiki.page.watch.ajax.js would know whether
the user has permissions to edit their watchlist. Alternatively,
it could have an interface to allow extensions to add their own
error handling. See T344925 for details.

Bug: T344870
Change-Id: Id1757fb4fb433fe39cf4d5c6e1e9c65ab5abae14
2023-08-24 14:12:43 +00:00

314 lines
10 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\Minerva\Menu\PageActions;
use ExtensionRegistry;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Minerva\LanguagesHelper;
use MediaWiki\Minerva\Menu\Entries\IMenuEntry;
use MediaWiki\Minerva\Menu\Entries\LanguageSelectorEntry;
use MediaWiki\Minerva\Menu\Entries\SingleMenuEntry;
use MediaWiki\Minerva\Menu\Group;
use MediaWiki\Minerva\Permissions\IMinervaPagePermissions;
use MediaWiki\Minerva\SkinOptions;
use MediaWiki\Minerva\Skins\SkinMinerva;
use MediaWiki\Minerva\Skins\SkinUserPageHelper;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use MediaWiki\Watchlist\WatchlistManager;
use MessageLocalizer;
use SpecialMobileHistory;
use SpecialPage;
use User;
class ToolbarBuilder {
/**
* @var User Currently logged in user
*/
private $user;
/**
* @var Title Article title user is currently browsing
*/
private $title;
/**
* @var MessageLocalizer Message localizer to generate localized texts
*/
private $messageLocalizer;
/**
* @var IMinervaPagePermissions
*/
private $permissions;
/**
* @var SkinOptions
*/
private $skinOptions;
/**
* @var SkinUserPageHelper
*/
private $relevantUserPageHelper;
/**
* @var LanguagesHelper
*/
private $languagesHelper;
/**
* @var bool Correlates to $wgWatchlistExpiry feature flag.
*/
private $watchlistExpiryEnabled;
/**
* @var WatchlistManager
*/
private $watchlistManager;
/**
* ServiceOptions needed.
*/
public const CONSTRUCTOR_OPTIONS = [
'WatchlistExpiry',
];
/**
* Build Group containing icons for toolbar
* @param Title $title Article title user is currently browsing
* @param User $user Currently logged in user
* @param MessageLocalizer $msgLocalizer Message localizer to generate localized texts
* @param IMinervaPagePermissions $permissions Minerva permissions system
* @param SkinOptions $skinOptions
* @param SkinUserPageHelper $relevantUserPageHelper User Page helper. The
* UserPageHelper passed should always be specific to the user page Title. If on a
* user talk page, UserPageHelper should be instantiated with the user page
* Title and NOT with the user talk page Title.
* @param LanguagesHelper $languagesHelper Helper to check title languages/variants
* @param ServiceOptions $options
* @param WatchlistManager $watchlistManager
*/
public function __construct(
Title $title,
User $user,
MessageLocalizer $msgLocalizer,
IMinervaPagePermissions $permissions,
SkinOptions $skinOptions,
SkinUserPageHelper $relevantUserPageHelper,
LanguagesHelper $languagesHelper,
ServiceOptions $options,
WatchlistManager $watchlistManager
) {
$this->title = $title;
$this->user = $user;
$this->messageLocalizer = $msgLocalizer;
$this->permissions = $permissions;
$this->skinOptions = $skinOptions;
$this->relevantUserPageHelper = $relevantUserPageHelper;
$this->languagesHelper = $languagesHelper;
$this->watchlistExpiryEnabled = $options->get( 'WatchlistExpiry' );
$this->watchlistManager = $watchlistManager;
}
/**
* @return Group
*/
public function getGroup(): Group {
$group = new Group( 'p-views' );
$permissions = $this->permissions;
$userPageOrUserTalkPageWithOverflowMode = $this->skinOptions->get( SkinOptions::TOOLBAR_SUBMENU )
&& $this->relevantUserPageHelper->isUserPage();
if ( !$userPageOrUserTalkPageWithOverflowMode && $permissions->isAllowed(
IMinervaPagePermissions::SWITCH_LANGUAGE ) ) {
$group->insertEntry( new LanguageSelectorEntry(
$this->title,
$this->languagesHelper->doesTitleHasLanguagesOrVariants( $this->title ),
$this->messageLocalizer,
true
) );
}
if ( $permissions->isAllowed( IMinervaPagePermissions::WATCHABLE ) ) {
$group->insertEntry( $this->createWatchPageAction() );
}
if ( $permissions->isAllowed( IMinervaPagePermissions::HISTORY ) ) {
$group->insertEntry( $this->getHistoryPageAction() );
}
$isUserPage = $this->relevantUserPageHelper->isUserPage();
$isUserPageAccessible = $this->relevantUserPageHelper->isUserPageAccessibleToCurrentUser();
if ( $isUserPage && $isUserPageAccessible ) {
// T235681: Contributions icon should be added to toolbar on user pages
// and user talk pages for all users
$user = $this->relevantUserPageHelper->getPageUser();
$group->insertEntry( $this->createContributionsPageAction( $user ) );
}
// We want the edit icon/action always to be the last element on the toolbar list
if ( $permissions->isAllowed( IMinervaPagePermissions::CONTENT_EDIT ) ) {
$group->insertEntry( $this->createEditPageAction() );
}
return $group;
}
/**
* Create Contributions page action visible on user pages or user talk pages
* for given $user
*
* @param UserIdentity $user Determines what the contribution page action will link to
* @return IMenuEntry
*/
protected function createContributionsPageAction( UserIdentity $user ): IMenuEntry {
$label = $this->messageLocalizer->msg( 'mobile-frontend-user-page-contributions' );
$entry = new SingleMenuEntry(
'page-actions-contributions',
$label->escaped(),
SpecialPage::getTitleFor( 'Contributions', $user->getName() )->getLocalURL() );
$entry->setTitle( $label )
->trackClicks( 'contributions' )
->setIcon( 'userContributions', 'element',
'mw-ui-icon-with-label-desktop'
);
return $entry;
}
/**
* Creates the "edit" page action: the well-known pencil icon that, when tapped, will open an
* editor with the lead section loaded.
*
* @return IMenuEntry An edit page actions menu entry
*/
protected function createEditPageAction(): IMenuEntry {
$title = $this->title;
$editArgs = [ 'action' => 'edit' ];
if ( $title->isWikitextPage() ) {
// If the content model is wikitext we'll default to editing the lead section.
// Full wikitext editing is hard on mobile devices.
$editArgs['section'] = SkinMinerva::LEAD_SECTION_NUMBER;
}
$editOrCreate = $this->permissions->isAllowed( IMinervaPagePermissions::EDIT_OR_CREATE );
$entry = new SingleMenuEntry(
'page-actions-edit',
$this->messageLocalizer->msg( 'mobile-frontend-editor-edit' )->escaped(),
$title->getLocalURL( $editArgs ),
'edit-page'
);
$entry->setIcon( $editOrCreate ? 'edit-base20' : 'editLock-base20',
'element', 'mw-ui-icon-with-label-desktop', 'wikimedia' )
->trackClicks( 'edit' )
->setTitle( $this->messageLocalizer->msg( 'mobile-frontend-pageaction-edit-tooltip' ) )
->setNodeID( 'ca-edit' );
return $entry;
}
/**
* Creates the "watch" or "unwatch" action: the well-known star icon that, when tapped, will
* add the page to or remove the page from the user's watchlist; or, if the user is logged out,
* will direct the user's UA to Special:Login.
*
* @return IMenuEntry An watch/unwatch page actions menu entry
*/
protected function createWatchPageAction(): IMenuEntry {
$isWatched = $this->user->isRegistered() &&
$this->watchlistManager->isWatched( $this->user, $this->title );
$isTempWatched = $this->watchlistExpiryEnabled &&
$isWatched &&
$this->watchlistManager->isTempWatched( $this->user, $this->title );
$newModeToSet = $isWatched ? 'unwatch' : 'watch';
$href = $this->user->isRegistered()
? $this->title->getLocalURL( [ 'action' => $newModeToSet ] )
: $this->getLoginUrl( [ 'returnto' => $this->title ] );
if ( $isWatched ) {
$msg = $this->messageLocalizer->msg( 'unwatch' );
$icon = $isTempWatched ? 'halfStar-progressive' : 'unStar-progressive';
} else {
$msg = $this->messageLocalizer->msg( 'watch' );
$icon = 'star-base20';
}
$btnClass = 'mw-ui-icon-with-label-desktop watch-this-article';
if ( $isTempWatched ) {
$btnClass .= ' temp-watched';
} elseif ( $isWatched ) {
$btnClass .= ' watched';
}
$entry = new SingleMenuEntry(
'page-actions-watch',
$msg->text(),
$href,
$btnClass . ' mw-watchlink'
);
return $entry->trackClicks( $newModeToSet )
->setIcon( $icon, 'element', '', 'wikimedia' )
->setTitle( $msg )
->setNodeID( 'ca-watch' );
}
/**
* Creates a history action: An icon that links to the mobile history page.
*
* @return IMenuEntry A menu entry object that represents a map of HTML attributes
* and a 'text' property to be used with the pageActionMenu.mustache template.
*/
protected function getHistoryPageAction(): IMenuEntry {
$entry = new SingleMenuEntry(
'page-actions-history',
$this->messageLocalizer->msg( 'minerva-page-actions-history' )->escaped(),
$this->getHistoryUrl( $this->title )
);
$entry->setIcon( 'history-base20', 'element', 'mw-ui-icon-with-label-desktop', 'wikimedia' )
->trackClicks( 'history' );
return $entry;
}
/**
* Get the URL for the history page for the given title using Special:History
* when available.
* FIXME: temporary duplicated code, same as SkinMinerva::getHistoryUrl()
* @param Title $title The Title object of the page being viewed
* @return string
*/
protected function getHistoryUrl( Title $title ) {
return ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) &&
SpecialMobileHistory::shouldUseSpecialHistory( $title, $this->user ) ?
SpecialPage::getTitleFor( 'History', $title )->getLocalURL() :
$title->getLocalURL( [ 'action' => 'history' ] );
}
/**
* Prepares a url to the Special:UserLogin with query parameters
* @param array $query
* @return string
*/
private function getLoginUrl( $query ) {
return SpecialPage::getTitleFor( 'Userlogin' )->getLocalURL( $query );
}
}