TempUser UI tweaks

* In PermissionManager, if a user is anonymous but temporary user
  creation is possible, grant elevated permissions at RIGOR_QUICK rigor
  level. This is mostly to make skins show "edit" instead of "view
  source" to anonymous users in the recommended permissions
  configuration.
* Present temporary users as if they are not logged in in various places
  in the interface: create/move permissions errors, login, preferences,
  watchlist, BotPasswords, ChangeEmail, ResetTokens.
* Show a warning on login/logout about loss of access to the temp
  account.
* On login, don't show the temporary name as a suggestion for the login
  username.

Change-Id: Id0d5ffa46c3ca5c7b30d540cedbaa528b682aa85
This commit is contained in:
Tim Starling 2022-04-11 11:39:33 +10:00
parent d6a3b6cfa8
commit 6c1f5462f7
20 changed files with 189 additions and 50 deletions

View file

@ -40,6 +40,7 @@ For notes on 1.38.x and older releases, see HISTORY.
* …
=== New user-facing features in 1.39 ===
* Optional automatic user creation on page save ($wgAutoCreateTempUser)
* Administrators now have the option to delete/undelete the associated "Talk"
page when they are (un)deleting a given page. `deletetalk` and `undeletetalk`
options were added to the 'delete' and 'undelete' action APIs in MW 1.38.

View file

@ -32,6 +32,8 @@ use MediaWiki\Page\PageIdentity;
use MediaWiki\Page\RedirectLookup;
use MediaWiki\Session\SessionManager;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\User\TempUser\TempUserConfig;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserGroupManager;
use MediaWiki\User\UserIdentity;
use MessageSpecifier;
@ -114,6 +116,12 @@ class PermissionManager {
/** @var TitleFormatter */
private $titleFormatter;
/** @var TempUserConfig */
private $tempUserConfig;
/** @var UserFactory */
private $userFactory;
/** @var string[][] Cached user rights */
private $usersRights = [];
@ -228,6 +236,8 @@ class PermissionManager {
* @param RedirectLookup $redirectLookup
* @param RestrictionStore $restrictionStore
* @param TitleFormatter $titleFormatter
* @param TempUserConfig $tempUserConfig
* @param UserFactory $userFactory
*/
public function __construct(
ServiceOptions $options,
@ -240,7 +250,9 @@ class PermissionManager {
UserCache $userCache,
RedirectLookup $redirectLookup,
RestrictionStore $restrictionStore,
TitleFormatter $titleFormatter
TitleFormatter $titleFormatter,
TempUserConfig $tempUserConfig,
UserFactory $userFactory
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
@ -254,6 +266,8 @@ class PermissionManager {
$this->redirectLookup = $redirectLookup;
$this->restrictionStore = $restrictionStore;
$this->titleFormatter = $titleFormatter;
$this->tempUserConfig = $tempUserConfig;
$this->userFactory = $userFactory;
}
/**
@ -409,6 +423,17 @@ class PermissionManager {
throw new Exception( "Invalid rigor parameter '$rigor'." );
}
// With RIGOR_QUICK we can assume automatic account creation will
// occur. At a higher rigor level, the caller is required to opt
// in by either setting the create intent or actually creating
// the account.
if ( $rigor === self::RIGOR_QUICK
&& !$user->isRegistered()
&& $this->tempUserConfig->isAutoCreateAction( $action )
) {
$user = $this->userFactory->newTempPlaceholder();
}
# Read has special handling
if ( $action == 'read' ) {
$checks = [
@ -888,7 +913,7 @@ class PermissionManager {
( !$this->nsInfo->isTalk( $title->getNamespace() ) &&
!$this->userHasRight( $user, 'createpage' ) )
) {
$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
$errors[] = $user->isNamed() ? [ 'nocreate-loggedin' ] : [ 'nocreatetext' ];
}
} elseif ( $action == 'move' ) {
if ( !$this->userHasRight( $user, 'move-rootuserpages' )
@ -914,11 +939,20 @@ class PermissionManager {
if ( !$this->userHasRight( $user, 'move' ) ) {
// User can't move anything
$userCanMove = $this->groupHasPermission( 'user', 'move' );
$autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' );
if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
$userCanMove = $this->groupPermissionsLookup
->groupHasPermission( 'user', 'move' );
$namedCanMove = $this->groupPermissionsLookup
->groupHasPermission( 'named', 'move' );
$autoconfirmedCanMove = $this->groupPermissionsLookup
->groupHasPermission( 'autoconfirmed', 'move' );
if ( $user->isAnon()
&& ( $userCanMove || $namedCanMove || $autoconfirmedCanMove )
) {
// custom message if logged-in users without any special rights can move
$errors[] = [ 'movenologintext' ];
} elseif ( $user->isTemp() && ( $namedCanMove || $autoconfirmedCanMove ) ) {
// Temp user may be able to move if they log in as a proper account
$errors[] = [ 'movenologintext' ];
} else {
$errors[] = [ 'movenotallowed' ];
}

View file

@ -1395,7 +1395,9 @@ return [
$services->getUserCache(),
$services->getRedirectLookup(),
$services->getRestrictionStore(),
$services->getTitleFormatter()
$services->getTitleFormatter(),
$services->getTempUserConfig(),
$services->getUserFactory()
);
},

View file

@ -94,8 +94,7 @@ class WatchAction extends FormAction {
}
protected function checkCanExecute( User $user ) {
// Must be logged in
if ( $user->isAnon() ) {
if ( !$user->isNamed() ) {
throw new UserNotLoggedIn( 'watchlistanontext', 'watchnologin' );
}

View file

@ -322,7 +322,11 @@ class CookieSessionProvider extends SessionProvider {
public function suggestLoginUsername( WebRequest $request ) {
$name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
if ( $name !== null ) {
$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_USABLE );
if ( $this->userNameUtils->isTemp( $name ) ) {
$name = false;
} else {
$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_USABLE );
}
}
return $name === false ? null : $name;
}

View file

@ -259,12 +259,16 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
* is present). People who often switch between several accounts have grown
* accustomed to this behavior.
*
* For temporary users, the form is always shown, since the UI presents
* temporary users as not logged in and offers to discard their temporary
* account by logging in.
*
* Also make an exception when force=<level> is set in the URL, which means the user must
* reauthenticate for security reasons.
*/
if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
$this->getUser()->isRegistered()
!$this->getUser()->isTemp() && $this->getUser()->isRegistered()
) {
$this->successfulAction();
return;
@ -328,7 +332,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
switch ( $response->status ) {
case AuthenticationResponse::PASS:
$this->logAuthResult( true );
$this->proxyAccountCreation = $this->isSignup() && !$this->getUser()->isAnon();
$this->proxyAccountCreation = $this->isSignup() && $this->getUser()->isNamed();
$this->targetUser = User::newFromName( $response->username );
if (
@ -566,6 +570,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
if (
!$this->isSignup() &&
$this->getUser()->isRegistered() &&
!$this->getUser()->isTemp() &&
$this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
) {
$reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
@ -728,9 +733,9 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
* @return array
*/
protected function getFieldDefinitions( array $fieldInfo ) {
$isRegistered = $this->getUser()->isRegistered();
$isLoggedIn = $this->getUser()->isRegistered();
$continuePart = $this->isContinued() ? 'continue-' : '';
$anotherPart = $isRegistered ? 'another-' : '';
$anotherPart = $isLoggedIn ? 'another-' : '';
// @phan-suppress-next-line PhanUndeclaredMethod
$expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
$expirationDays = ceil( $expiration / ( 3600 * 24 ) );
@ -763,7 +768,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
'username' => [
'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink,
'id' => 'wpName2',
'placeholder-message' => $isRegistered ? 'createacct-another-username-ph'
'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
: 'userlogin-yourname-ph',
],
'mailpassword' => [
@ -829,7 +834,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
],
'realname' => [
'type' => 'text',
'help-message' => $isRegistered ? 'createacct-another-realname-tip'
'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
: 'prefs-help-realname',
'label-message' => 'createacct-realname',
'cssclass' => 'loginText',
@ -981,6 +986,17 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
'weight' => -100,
];
}
if ( $this->isSignup() && $this->getUser()->isTemp() ) {
$fieldDefinitions['tempWarning'] = [
'type' => 'info',
'default' => Html::warningBox(
$this->msg( 'createacct-temp-warning' )->parse()
),
'raw' => true,
'rawrow' => true,
'weight' => -90,
];
}
if ( !$this->showExtraInformation() ) {
unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
}
@ -1025,26 +1041,27 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
if ( $this->mLanguage ) {
$linkq .= '&uselang=' . urlencode( $this->mLanguage );
}
$isRegistered = $this->getUser()->isRegistered();
$isLoggedIn = $this->getUser()->isRegistered()
&& !$this->getUser()->isTemp();
$fieldDefinitions['createOrLogin'] = [
'type' => 'info',
'raw' => true,
'linkQuery' => $linkq,
'default' => function ( $params ) use ( $isRegistered, $linkTitle ) {
'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) {
return Html::rawElement( 'div',
[ 'id' => 'mw-createaccount' . ( !$isRegistered ? '-cta' : '' ),
'class' => ( $isRegistered ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
( $isRegistered ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
[ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ),
'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
. Html::element( 'a',
[
'id' => 'mw-createaccount-join' . ( $isRegistered ? '-loggedin' : '' ),
'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ),
'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
'class' => ( $isRegistered ? '' : 'mw-ui-button' ),
'class' => ( $isLoggedIn ? '' : 'mw-ui-button' ),
'tabindex' => 100,
],
$this->msg(
$isRegistered ? 'userlogin-createanother' : 'userlogin-joinproject'
$isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
)->text()
)
);
@ -1177,7 +1194,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
!$this->isSignup()
) {
$user = $this->getUser();
if ( $user->isRegistered() ) {
if ( $user->isRegistered() && !$user->isTemp() ) {
$formDescriptor['username']['default'] = $user->getName();
} else {
$formDescriptor['username']['default'] =

View file

@ -418,6 +418,22 @@ class SpecialPage implements MessageLocalizer {
}
}
/**
* If the user is not logged in or is a temporary user, throws UserNotLoggedIn
*
* @since 1.39
* @param string $reasonMsg [optional] Message key to be displayed on login page
* @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor
* @throws UserNotLoggedIn
*/
public function requireNamedUser(
$reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin'
) {
if ( !$this->getUser()->isNamed() ) {
throw new UserNotLoggedIn( $reasonMsg, $titleMsg );
}
}
/**
* Tells if the special page does something security-sensitive and needs extra defense against
* a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the

View file

@ -87,7 +87,7 @@ class SpecialBotPasswords extends FormSpecialPage {
*/
public function execute( $par ) {
$this->getOutput()->disallowUserJs();
$this->requireLogin();
$this->requireNamedUser();
$this->addHelpLink( 'Manual:Bot_passwords' );
$par = trim( $par );

View file

@ -75,7 +75,7 @@ class SpecialChangeEmail extends FormSpecialPage {
throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
}
$this->requireLogin( 'changeemail-no-info' );
$this->requireNamedUser( 'changeemail-no-info' );
// This could also let someone check the current email address, so
// require both permissions.

View file

@ -124,7 +124,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$this->setHeaders();
# Anons don't get a watchlist
$this->requireLogin( 'watchlistanontext' );
$this->requireNamedUser( 'watchlistanontext' );
$out = $this->getOutput();

View file

@ -63,7 +63,7 @@ class SpecialPreferences extends SpecialPage {
$out = $this->getOutput();
$out->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
$this->requireLogin( 'prefsnologintext2' );
$this->requireNamedUser( 'prefsnologintext2' );
$this->checkReadOnly();
if ( $par == 'reset' ) {

View file

@ -71,7 +71,7 @@ class SpecialResetTokens extends FormSpecialPage {
public function execute( $par ) {
// This is a preferences page, so no user JS for y'all.
$this->getOutput()->disallowUserJs();
$this->requireLogin();
$this->requireNamedUser();
parent::execute( $par );

View file

@ -63,7 +63,9 @@ class SpecialUserLogout extends FormSpecialPage {
public function alterForm( HTMLForm $form ) {
$form->setTokenSalt( 'logoutToken' );
$form->addHeaderText( $this->msg( 'userlogout-continue' ) );
$form->addHeaderHtml( $this->msg(
$this->getUser()->isTemp() ? 'userlogout-temp' : 'userlogout-continue'
) );
$form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
}

View file

@ -86,7 +86,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
*/
public function execute( $subpage ) {
// Anons don't get a watchlist
$this->requireLogin( 'watchlistanontext' );
$this->requireNamedUser( 'watchlistanontext' );
$output = $this->getOutput();
$request = $this->getRequest();

View file

@ -479,6 +479,7 @@
"nocookiesfornew": "The user account was not created, as we could not confirm its source.\nEnsure you have cookies enabled, reload this page and try again.",
"nocookiesforlogin": "{{int:nocookieslogin}}",
"createacct-loginerror": "The account was successfully created but you could not be logged in automatically. Please proceed to [[Special:UserLogin|manual login]].",
"createacct-temp-warning": "The edits you made with your temporary account will not be carried over to your permanent one.",
"noname": "You have not specified a valid username.",
"loginsuccesstitle": "Logged in",
"loginsuccess": "<strong>You are now logged in to {{SITENAME}} as \"$1\".</strong>",
@ -4420,6 +4421,7 @@
"deflate-invaliddeflate": "Content provided is not properly deflated",
"unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
"userlogout-continue": "Do you want to log out?",
"userlogout-temp": "Are you sure you want to log out? There will be no way to log back in to your temporary account.",
"paramvalidator-baduser": "Invalid value \"$2\" for user parameter <var>$1</var>.",
"paramvalidator-help-type-user": "Type: {{PLURAL:$1|1=user|2=list of users}}, {{PLURAL:$3|by|by any of}} $2",
"paramvalidator-help-type-user-subtype-name": "user name",

View file

@ -713,6 +713,7 @@
"nocookiesfornew": "This message is displayed when the user tried to create a new account, but it failed the cross-site request forgery (CSRF) check. It could be blocking an attack, but most likely, the browser isn't accepting cookies.",
"nocookiesforlogin": "{{optional}}\nThis message is displayed when someone tried to login and the CSRF failed (most likely, the browser doesn't accept cookies).\n\nDefault:\n* {{msg-mw|Nocookieslogin}}",
"createacct-loginerror": "This message is displayed after a successful registration when there is a server-side error with logging the user in. This is not expected to happen.",
"createacct-temp-warning": "Message shown to temporary users when creating an account.",
"noname": "Error message.",
"loginsuccesstitle": "The title of the page saying that you are logged in. The content of the page is the message {{msg-mw|Loginsuccess}}.\n{{Identical|Log in}}",
"loginsuccess": "The content of the page saying that you are logged in. The title of the page is {{msg-mw|Loginsuccesstitle}}.\n\nParameters:\n* $1 - the name of the logged in user\n{{Gender}}",
@ -4654,6 +4655,7 @@
"deflate-invaliddeflate": "Error message if the content passed to Deflate was not deflated (compressed) properly",
"unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
"userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out.",
"userlogout-temp": "Shown if the a temporary (cookie-only) user is attempting to log out.",
"paramvalidator-baduser": "Error in API parameter validation. Parameters:\n* $1 - Parameter name.\n* $2 - Parameter value.",
"paramvalidator-help-type-user": "Used to indicate that a parameter is a user or list of users. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - List of allowed ways to specify the user, as an 'and'-style list.\n* $3 - Number of items in $2.\n\nSee also:\n* {{msg-mw|paramvalidator-help-type-user-subtype-name}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-ip}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-cidr}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-interwiki}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-id}}",
"paramvalidator-help-type-user-subtype-name": "Used with {{msg-mw|paramvalidator-help-type-user}} to indicate that users may be specified by name.",

View file

@ -11,6 +11,7 @@ use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\SystemBlock;
use MediaWiki\Cache\CacheKeyHelper;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Session\SessionId;
use MediaWiki\Session\TestUtils;
@ -427,6 +428,42 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
$this->assertTrue( $permissionManager->userCan( 'edit', $this->user, $title ) );
}
public function testAutocreatePermissionsHack() {
$this->setMwGlobals( [
'wgAutoCreateTempUser' => [
'enabled' => true,
'actions' => [ 'edit' ],
'serialProvider' => [ 'type' => 'local' ],
'serialMapping' => [ 'type' => 'plain-numeric' ],
'matchPattern' => '*Unregistered $1',
'genPattern' => '*Unregistered $1'
],
'wgGroupPermissions' => [
'*' => [ 'edit' => false ],
'user' => [ 'edit' => true, 'createpage' => true ],
]
] );
$services = $this->getServiceContainer();
$permissionManager = $services->getPermissionManager();
$user = $services->getUserFactory()->newAnonymous();
$title = $this->getNonexistingTestPage()->getTitle();
$this->assertNotEmpty(
$permissionManager->getPermissionErrors(
'edit',
$user,
$title
)
);
$this->assertEmpty(
$permissionManager->getPermissionErrors(
'edit',
$user,
$title,
PermissionManager::RIGOR_QUICK
)
);
}
/**
* @dataProvider provideTestCheckUserBlockActions
* @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock

View file

@ -159,6 +159,7 @@ class WatchActionTest extends MediaWikiIntegrationTestCase {
public function testShowUserLoggedInNoException() {
$registeredUser = $this->createMock( User::class );
$registeredUser->method( 'isRegistered' )->willReturn( true );
$registeredUser->method( 'isNamed' )->willReturn( true );
$testContext = new DerivativeContext( $this->watchAction->getContext() );
$testContext->setUser( $registeredUser );
$watchAction = $this->getWatchAction(

View file

@ -27,6 +27,8 @@ class SpecialPreferencesTest extends MediaWikiIntegrationTestCase {
$user = $this->createMock( User::class );
$user->method( 'isAnon' )
->willReturn( false );
$user->method( 'isNamed' )
->willReturn( true );
# The mocked user has a long nickname
$user->method( 'getOption' )

View file

@ -11,6 +11,8 @@ use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Permissions\RestrictionStore;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\User\TempUser\RealTempUserConfig;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserGroupManager;
use MediaWikiUnitTestCase;
use Title;
@ -71,6 +73,10 @@ class PermissionManagerTest extends MediaWikiUnitTestCase {
$this->createMock( RestrictionStore::class );
$titleFormatter = $options['titleFormatter'] ??
$this->createMock( TitleFormatter::class );
$tempUserConfig = $options['tempUserConfig'] ??
new RealTempUserConfig( [] );
$userFactory = $options['userFactory'] ??
$this->createMock( UserFactory::class );
$permissionManager = new PermissionManager(
new ServiceOptions( PermissionManager::CONSTRUCTOR_OPTIONS, $config ),
@ -83,7 +89,9 @@ class PermissionManagerTest extends MediaWikiUnitTestCase {
$userCache,
$redirectLookup,
$restrictionStore,
$titleFormatter
$titleFormatter,
$tempUserConfig,
$userFactory
);
$accessPermissionManager = TestingAccessWrapper::newFromObject( $permissionManager );
@ -342,7 +350,7 @@ class PermissionManagerTest extends MediaWikiUnitTestCase {
public function testCheckQuickPermissions(
int $namespace,
string $pageTitle,
bool $userIsAnon,
string $userType,
string $action,
array $rights,
string $expectedError
@ -350,10 +358,15 @@ class PermissionManagerTest extends MediaWikiUnitTestCase {
// Convert string single error to the array of errors PermissionManager uses
$expectedErrors = ( $expectedError === '' ? [] : [ [ $expectedError ] ] );
$userIsAnon = $userType === 'anon';
$userIsTemp = $userType === 'temp';
$userIsNamed = $userType === 'user';
$user = $this->createMock( User::class );
$user->method( 'getId' )->willReturn( $userIsAnon ? 0 : 123 );
$user->method( 'getName' )->willReturn( $userIsAnon ? '1.1.1.1' : 'NameOfActingUser' );
$user->method( 'isAnon' )->willReturn( $userIsAnon );
$user->method( 'isNamed' )->willReturn( $userIsNamed );
$user->method( 'isTemp' )->willReturn( $userIsTemp );
// HookContainer - always return true (false tested separately)
$hookContainer = $this->createMock( HookContainer::class );
@ -398,62 +411,69 @@ class PermissionManagerTest extends MediaWikiUnitTestCase {
// $namespace, $pageTitle, $userIsAnon, $action, $rights, $expectedError
// Four different possible errors when trying to create
yield 'Anon createtalk fail' => [
NS_TALK, 'Example', true, 'create', [], 'nocreatetext'
NS_TALK, 'Example', 'anon', 'create', [], 'nocreatetext'
];
yield 'Anon createpage fail' => [
NS_MAIN, 'Example', true, 'create', [], 'nocreatetext'
NS_MAIN, 'Example', 'anon', 'create', [], 'nocreatetext'
];
yield 'User createtalk fail' => [
NS_TALK, 'Example', false, 'create', [], 'nocreate-loggedin'
NS_TALK, 'Example', 'user', 'create', [], 'nocreate-loggedin'
];
yield 'User createpage fail' => [
NS_MAIN, 'Example', false, 'create', [], 'nocreate-loggedin'
NS_MAIN, 'Example', 'user', 'create', [], 'nocreate-loggedin'
];
yield 'Temp user createpage fail' => [
NS_MAIN, 'Example', 'temp', 'create', [], 'nocreatetext'
];
yield 'Createpage pass' => [
NS_MAIN, 'Example', true, 'create', [ 'createpage' ], ''
NS_MAIN, 'Example', 'anon', 'create', [ 'createpage' ], ''
];
// Three different namespace specific move failures, even if user has `move` rights
yield 'Move root user page fail' => [
NS_USER, 'Example', true, 'move', [ 'move' ], 'cant-move-user-page'
NS_USER, 'Example', 'anon', 'move', [ 'move' ], 'cant-move-user-page'
];
yield 'Move file fail' => [
NS_FILE, 'Example', true, 'move', [ 'move' ], 'movenotallowedfile'
NS_FILE, 'Example', 'anon', 'move', [ 'move' ], 'movenotallowedfile'
];
yield 'Move category fail' => [
NS_CATEGORY, 'Example', true, 'move', [ 'move' ], 'cant-move-category-page'
NS_CATEGORY, 'Example', 'anon', 'move', [ 'move' ], 'cant-move-category-page'
];
// No move rights at all. Different failures depending on who is allowed to move.
// Test method sets group permissions to [ 'autoconfirmed' => [ 'move' => true ] ]
yield 'Anon move fail, autoconfirmed can move' => [
NS_TALK, 'Example', true, 'move', [], 'movenologintext'
NS_TALK, 'Example', 'anon', 'move', [], 'movenologintext'
];
yield 'User move fail, autoconfirmed can move' => [
NS_TALK, 'Example', false, 'move', [], 'movenotallowed'
NS_TALK, 'Example', 'user', 'move', [], 'movenotallowed'
];
yield 'Move pass' => [ NS_MAIN, 'Example', true, 'move', [ 'move' ], '' ];
yield 'Temp user move fail, autoconfirmed can move' => [
NS_TALK, 'Example', 'temp', 'move', [], 'movenologintext'
];
yield 'Move pass' => [ NS_MAIN, 'Example', 'anon', 'move', [ 'move' ], '' ];
// Three different possible failures for move target
yield 'Move-target no rights' => [
NS_MAIN, 'Example', false, 'move-target', [], 'movenotallowed'
NS_MAIN, 'Example', 'user', 'move-target', [], 'movenotallowed'
];
yield 'Move-target to user root' => [
NS_USER, 'Example', false, 'move-target', [ 'move' ], 'cant-move-to-user-page'
NS_USER, 'Example', 'user', 'move-target', [ 'move' ], 'cant-move-to-user-page'
];
yield 'Move-target to category' => [
NS_CATEGORY, 'Example', false, 'move-target', [ 'move' ], 'cant-move-to-category-page'
NS_CATEGORY, 'Example', 'user', 'move-target', [ 'move' ], 'cant-move-to-category-page'
];
yield 'Move-target pass' => [
NS_MAIN, 'Example', false, 'move-target', [ 'move' ], ''
NS_MAIN, 'Example', 'user', 'move-target', [ 'move' ], ''
];
// Other actions without special handling
yield 'Missing rights for edit' => [
NS_MAIN, 'Example', false, 'edit', [], 'badaccess-group0'
NS_MAIN, 'Example', 'user', 'edit', [], 'badaccess-group0'
];
yield 'Having rights for edit' => [
NS_MAIN, 'Example', false, 'edit', [ 'edit', ], ''
NS_MAIN, 'Example', 'user', 'edit', [ 'edit', ], ''
];
}