TempUser EditPage and permissions

* Allow EditPage to create a user on page save. This has to be enabled
  in config and then activated by the UI/API caller.
* Add an autocreate source for temporary users.
* Allow editing by anonymous users via automatic account creation when
  $wgGroupPermisions['*']['edit'] = false. On an edit GET request, use
  an unsaved placeholder user to stand in for post-create permissions.
* On preview or aborted save, the username to be created is stashed in a
  session and restored on subsequent requests.
* On a (likely) successful page save, create the account.
* Put regular non-temporary users in a "named" group so that they can be
  given additional permissions.
* Use a different "~~~" signature for temporary users
* Show account creation warnings on edit and preview.

Change-Id: I67b23abf73cc371280bfb2b6c43b3ce0e077bfe5
This commit is contained in:
Tim Starling 2022-04-11 11:26:51 +10:00
parent 6393713b44
commit d6a3b6cfa8
16 changed files with 452 additions and 72 deletions

View file

@ -61,6 +61,9 @@ use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\RevisionStoreRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\EditResult;
use MediaWiki\Storage\PageUpdater;
use MediaWiki\User\TempUser\TempUserCreator;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserOptionsLookup;
@ -437,6 +440,30 @@ class EditPage implements IEditObject {
/** @var UserOptionsLookup */
private $userOptionsLookup;
/** @var TempUserCreator */
private $tempUserCreator;
/** @var UserFactory */
private $userFactory;
/** @var User|null */
private $placeholderTempUser;
/** @var User|null */
private $unsavedTempUser;
/** @var User|null */
private $savedTempUser;
/** @var bool Whether temp user creation will be attempted */
private $tempUserCreateActive = false;
/** @var string|null If a temp user name was acquired, this is the name */
private $tempUserName;
/** @var bool Whether temp user creation was successful */
private $tempUserCreateDone = false;
/**
* @stable to call
* @param Article $article
@ -473,6 +500,8 @@ class EditPage implements IEditObject {
$this->userNameUtils = $services->getUserNameUtils();
$this->redirectLookup = $services->getRedirectLookup();
$this->userOptionsLookup = $services->getUserOptionsLookup();
$this->tempUserCreator = $services->getTempUserCreator();
$this->userFactory = $services->getUserFactory();
$this->deprecatePublicProperty( 'mArticle', '1.30', __CLASS__ );
$this->deprecatePublicProperty( 'mTitle', '1.30', __CLASS__ );
@ -604,16 +633,19 @@ class EditPage implements IEditObject {
}
}
$this->maybeActivateTempUserCreate( !$this->firsttime );
$permErrors = $this->getEditPermissionErrors(
$this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
);
if ( $permErrors ) {
wfDebug( __METHOD__ . ": User can't edit" );
if ( $this->context->getUser()->getBlock() && !$readOnlyMode->isReadOnly() ) {
$user = $this->context->getUser();
if ( $user->getBlock() && !$readOnlyMode->isReadOnly() ) {
// Auto-block user's IP if the account was "hard" blocked
DeferredUpdates::addCallableUpdate( function () {
$this->context->getUser()->spreadAnyEditBlock();
DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
$user->spreadAnyEditBlock();
} );
}
$this->displayPermissionsError( $permErrors );
@ -734,12 +766,147 @@ class EditPage implements IEditObject {
$this->showEditForm();
}
/**
* Check the configuration and current user and enable automatic temporary
* user creation if possible.
*
* @param bool $doAcquire Whether to acquire a name for the temporary account
*
* @since 1.39
*/
public function maybeActivateTempUserCreate( $doAcquire ) {
if ( $this->tempUserCreateActive ) {
// Already done
return;
}
$user = $this->context->getUser();
if ( !$user->isRegistered()
&& $this->tempUserCreator->isAutoCreateAction( 'edit' )
&& $this->permManager->userHasRight( $user, 'createaccount' )
) {
if ( $doAcquire ) {
$name = $this->tempUserCreator->acquireAndStashName(
$this->context->getRequest()->getSession() );
$this->unsavedTempUser = $this->userFactory->newUnsavedTempUser( $name );
$this->tempUserName = $name;
} else {
$this->placeholderTempUser = $this->userFactory->newTempPlaceholder();
}
$this->tempUserCreateActive = true;
}
}
/**
* If automatic user creation is enabled, create the user and adjust the
* PageUpdater so that it has the new user/actor ID.
*
* This is a helper for internalAttemptSave(). The name should have already
* been acquired at this point for PST purposes, but if not, it will be
* acquired here.
*
* If the edit is a null edit, the user will not be created.
*
* @param PageUpdater $pageUpdater
* @return Status
*/
private function createTempUser( PageUpdater $pageUpdater ) {
if ( !$this->tempUserCreateActive ) {
return Status::newGood();
}
if ( !$pageUpdater->isChange() ) {
$pageUpdater->preventChange();
return Status::newGood();
}
$status = $this->tempUserCreator->create(
$this->tempUserName, // acquire if null
$this->context->getRequest()
);
if ( $status->isOK() ) {
$this->placeholderTempUser = null;
$this->unsavedTempUser = null;
$this->savedTempUser = $status->getUser();
$pageUpdater->updateAuthor( $status->getUser() );
$this->tempUserCreateDone = true;
}
return $status;
}
/**
* Get the authority for permissions purposes.
*
* On an initial edit page GET request, if automatic temporary user creation
* is enabled, this may be a placeholder user with a fixed name. Such users
* are unsuitable for anything that uses or exposes the name, like
* throttling. The only thing a placeholder user is good for is fooling the
* permissions system into allowing edits by anons.
*
* @return Authority
*/
private function getAuthority(): Authority {
return $this->getUserForPermissions();
}
/**
* Get the user for permissions purposes, with declared type User instead
* of Authority for compatibility with PermissionManager.
*
* @return User
*/
private function getUserForPermissions() {
if ( $this->savedTempUser ) {
return $this->savedTempUser;
} elseif ( $this->unsavedTempUser ) {
return $this->unsavedTempUser;
} elseif ( $this->placeholderTempUser ) {
return $this->placeholderTempUser;
} else {
return $this->context->getUser();
}
}
/**
* Get the user for preview or PST purposes. During the temporary user
* creation flow this may be an unsaved temporary user.
*
* @return User
*/
private function getUserForPreview() {
if ( $this->savedTempUser ) {
return $this->savedTempUser;
} elseif ( $this->unsavedTempUser ) {
return $this->unsavedTempUser;
} elseif ( $this->tempUserCreateActive ) {
throw new MWException(
"Can't use the request user for preview with IP masking enabled" );
} else {
return $this->context->getUser();
}
}
/**
* Get the user suitable for permanent attribution in the database. This
* asserts that an anonymous user won't be used in IP masking mode.
*
* @return User
*/
private function getUserForSave() {
if ( $this->savedTempUser ) {
return $this->savedTempUser;
} elseif ( $this->tempUserCreateActive ) {
throw new MWException(
"Can't use the request user for storage with IP masking enabled" );
} else {
return $this->context->getUser();
}
}
/**
* @param string $rigor PermissionManager::RIGOR_ constant
* @return array
*/
private function getEditPermissionErrors( string $rigor = PermissionManager::RIGOR_SECURE ): array {
$user = $this->context->getUser();
$user = $this->getUserForPermissions();
$ignoredErrors = [];
if ( $this->preview || $this->diff ) {
$ignoredErrors = [ 'blockedtext', 'autoblockedtext', 'systemblockedtext' ];
@ -1038,7 +1205,7 @@ class EditPage implements IEditObject {
$this->recreate = $request->getCheck( 'wpRecreate' );
$user = $this->getContext()->getUser();
$user = $this->context->getUser();
$this->minoredit = $request->getCheck( 'wpMinoredit' );
$this->watchthis = $request->getCheck( 'wpWatchthis' );
@ -1257,7 +1424,6 @@ class EditPage implements IEditObject {
$content = false;
$user = $this->context->getUser();
$request = $this->context->getRequest();
// For message page not locally set, use the i18n message.
// For other non-existent articles, use preload text if any.
@ -1285,7 +1451,7 @@ class EditPage implements IEditObject {
// For existing pages, get text based on "undo" or section parameters.
} elseif ( $this->section !== '' ) {
// Get section edit text (returns $def_text for invalid sections)
$orig = $this->getOriginalContent( $user );
$orig = $this->getOriginalContent( $this->getAuthority() );
$content = $orig ? $orig->getSection( $this->section ) : null;
if ( !$content ) {
@ -1333,9 +1499,14 @@ class EditPage implements IEditObject {
if ( $undoMsg === null ) {
$oldContent = $this->page->getContent( RevisionRecord::RAW );
$services = MediaWikiServices::getInstance();
$popts = ParserOptions::newFromUserAndLang( $user, $services->getContentLanguage() );
$popts = ParserOptions::newFromUserAndLang(
$this->getUserForPreview(),
$services->getContentLanguage()
);
$contentTransformer = $services->getContentTransformer();
$newContent = $contentTransformer->preSaveTransform( $content, $this->mTitle, $user, $popts );
$newContent = $contentTransformer->preSaveTransform(
$content, $this->mTitle, $this->getUserForPreview(), $popts
);
if ( $newContent->getModel() !== $oldContent->getModel() ) {
// The undo may change content
@ -1439,7 +1610,7 @@ class EditPage implements IEditObject {
}
if ( $content === false ) {
$content = $this->getOriginalContent( $user );
$content = $this->getOriginalContent( $this->getAuthority() );
}
}
@ -1620,7 +1791,7 @@ class EditPage implements IEditObject {
$content = $converted;
}
$parserOptions = ParserOptions::newFromUser( $this->context->getUser() );
$parserOptions = ParserOptions::newFromUser( $this->getUserForPreview() );
return MediaWikiServices::getInstance()->getContentTransformer()->preloadTransform(
$content,
$title,
@ -1694,11 +1865,11 @@ class EditPage implements IEditObject {
public function attemptSave( &$resultDetails = false ) {
// Allow bots to exempt some edits from bot flagging
$markAsBot = $this->markAsBot
&& $this->context->getAuthority()->isAllowed( 'bot' );
&& $this->getAuthority()->isAllowed( 'bot' );
// Allow trusted users to mark some edits as minor
$markAsMinor = $this->minoredit && !$this->isNew
&& $this->context->getAuthority()->isAllowed( 'minoredit' );
&& $this->getAuthority()->isAllowed( 'minoredit' );
$status = $this->internalAttemptSave( $resultDetails, $markAsBot, $markAsMinor );
@ -1957,7 +2128,10 @@ class EditPage implements IEditObject {
}
$this->contentLength = strlen( $this->textbox1 );
$user = $this->context->getUser();
$requestUser = $this->context->getUser();
$authority = $this->getAuthority();
$pstUser = $this->getUserForPreview();
$changingContentModel = false;
if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
@ -1977,10 +2151,11 @@ class EditPage implements IEditObject {
// SimpleAntiSpamConstraint: ensure that the context request does not have
// `wpAntispam` set
// Use $user since there is no permissions aspect
$constraintRunner->addConstraint(
$constraintFactory->newSimpleAntiSpamConstraint(
$this->context->getRequest()->getText( 'wpAntispam' ),
$user,
$requestUser,
$this->mTitle
)
);
@ -1997,21 +2172,21 @@ class EditPage implements IEditObject {
)
);
$constraintRunner->addConstraint(
new EditRightConstraint( $user )
new EditRightConstraint( $authority )
);
$constraintRunner->addConstraint(
new ImageRedirectConstraint(
$textbox_content,
$this->mTitle,
$user
$authority
)
);
$constraintRunner->addConstraint(
$constraintFactory->newUserBlockConstraint( $this->mTitle, $user )
$constraintFactory->newUserBlockConstraint( $this->mTitle, $requestUser )
);
$constraintRunner->addConstraint(
new ContentModelChangeConstraint(
$user,
$authority,
$this->mTitle,
$this->contentModel
)
@ -2021,7 +2196,7 @@ class EditPage implements IEditObject {
$constraintFactory->newReadOnlyConstraint()
);
$constraintRunner->addConstraint(
new UserRateLimitConstraint( $user, $this->mTitle, $this->contentModel )
new UserRateLimitConstraint( $requestUser, $this->mTitle, $this->contentModel )
);
$constraintRunner->addConstraint(
// Same constraint is used to check size before and after merging the
@ -2032,7 +2207,7 @@ class EditPage implements IEditObject {
)
);
$constraintRunner->addConstraint(
new ChangeTagsConstraint( $user, $this->changeTags )
new ChangeTagsConstraint( $authority, $this->changeTags )
);
// If the article has been deleted while editing, don't save it without
@ -2060,8 +2235,8 @@ class EditPage implements IEditObject {
}
// END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
# Load the page data from the primary DB. If anything changes in the meantime,
# we detect it by using page_latest like a token in a 1 try compare-and-swap.
// Load the page data from the primary DB. If anything changes in the meantime,
// we detect it by using page_latest like a token in a 1 try compare-and-swap.
$this->page->loadPageData( WikiPage::READ_LATEST );
$new = !$this->page->exists();
@ -2088,7 +2263,7 @@ class EditPage implements IEditObject {
$result['sectionanchor'] = $anchor;
}
$pageUpdater = $this->page->newPageUpdater( $user )
$pageUpdater = $this->page->newPageUpdater( $pstUser )
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
->setContent( SlotRecord::MAIN, $content );
$pageUpdater->prepareUpdate( $flags );
@ -2098,7 +2273,7 @@ class EditPage implements IEditObject {
$constraintRunner = new EditConstraintRunner();
// Late check for create permission, just in case *PARANOIA*
$constraintRunner->addConstraint(
new CreationPermissionConstraint( $user, $this->mTitle )
new CreationPermissionConstraint( $authority, $this->mTitle )
);
// Don't save a new page if it's blank or if it's a MediaWiki:
@ -2119,7 +2294,7 @@ class EditPage implements IEditObject {
$this->summary,
$markAsMinor,
$this->context->getLanguage(),
$user
$pstUser
)
);
@ -2151,7 +2326,7 @@ class EditPage implements IEditObject {
$this->isConflict = true;
[ $newSectionSummary, $newSectionAnchor ] = $this->newSectionSummary();
if ( $this->section === 'new' ) {
if ( $this->page->getUserText() === $user->getName() &&
if ( $this->page->getUserText() === $requestUser->getName() &&
$this->page->getComment() === $newSectionSummary
) {
// Probably a duplicate submission of a new comment.
@ -2170,7 +2345,7 @@ class EditPage implements IEditObject {
&& $this->revisionStore->userWasLastToEdit(
wfGetDB( DB_PRIMARY ),
$this->mTitle->getArticleID(),
$user->getId(),
$requestUser->getId(),
$this->edittime
)
) {
@ -2252,7 +2427,7 @@ class EditPage implements IEditObject {
return Status::newGood( self::AS_CONFLICT_DETECTED )->setOK( false );
}
$pageUpdater = $this->page->newPageUpdater( $user )
$pageUpdater = $this->page->newPageUpdater( $pstUser )
->setContent( SlotRecord::MAIN, $content );
$pageUpdater->prepareUpdate( $flags );
@ -2266,7 +2441,7 @@ class EditPage implements IEditObject {
$this->summary,
$markAsMinor,
$this->context->getLanguage(),
$user
$requestUser
)
);
@ -2288,7 +2463,7 @@ class EditPage implements IEditObject {
$this->allowBlankSummary,
$content,
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable FIXME T301947
$this->getOriginalContent( $user )
$this->getOriginalContent( $authority )
)
);
}
@ -2358,6 +2533,12 @@ class EditPage implements IEditObject {
}
// END OF MIGRATION TO EDITCONSTRAINT SYSTEM
// Auto-create the user if that is enabled
$status = $this->createTempUser( $pageUpdater );
if ( !$status->isOK() ) {
return $status;
}
if ( $this->undidRev && $this->isUndoClean( $content ) ) {
// As the user can change the edit's content before saving, we only mark
// "clean" undos as reverts. This is to avoid abuse by marking irrelevant
@ -2372,9 +2553,7 @@ class EditPage implements IEditObject {
}
$needsPatrol = $useRCPatrol || ( $useNPPatrol && !$this->page->exists() );
if ( $needsPatrol && $this->context->getAuthority()
->authorizeWrite( 'autopatrol', $this->getTitle() )
) {
if ( $needsPatrol && $authority->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
$pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
}
@ -2406,7 +2585,7 @@ class EditPage implements IEditObject {
$result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
if ( $result['nullEdit'] ) {
// We don't know if it was a null edit until now, so increment here
$user->pingLimiter( 'linkpurge' );
$requestUser->pingLimiter( 'linkpurge' );
}
$result['redirect'] = $content->isRedirect();
@ -2415,7 +2594,7 @@ class EditPage implements IEditObject {
// If the content model changed, add a log entry
if ( $changingContentModel ) {
$this->addContentModelChangeLogEntry(
$user,
$this->getUserForSave(),
// @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
// $oldContentModel is set when $changingContentModel is true
$new ? false : $oldContentModel,
@ -2503,7 +2682,7 @@ class EditPage implements IEditObject {
// Do a pre-save transform on the retrieved undo content
$services = MediaWikiServices::getInstance();
$contentLanguage = $services->getContentLanguage();
$user = $this->context->getUser();
$user = $this->getUserForPreview();
$parserOptions = ParserOptions::newFromUserAndLang( $user, $contentLanguage );
$contentTransformer = $services->getContentTransformer();
$undoContent = $contentTransformer->preSaveTransform( $undoContent, $this->mTitle, $user, $parserOptions );
@ -2538,8 +2717,8 @@ class EditPage implements IEditObject {
* Register the change of watch status
*/
private function updateWatchlist(): void {
$performer = $this->context->getAuthority();
if ( !$performer->getUser()->isRegistered() ) {
$user = $this->getUserForSave();
if ( !$user->isNamed() ) {
return;
}
@ -2550,7 +2729,7 @@ class EditPage implements IEditObject {
// This can't run as a DeferredUpdate due to a possible race condition
// when the post-edit redirect happens if the pendingUpdates queue is
// too large to finish in time (T259564)
$this->watchlistManager->setWatch( $watch, $performer, $title, $watchlistExpiry );
$this->watchlistManager->setWatch( $watch, $user, $title, $watchlistExpiry );
$this->watchedItemStore->maybeEnqueueWatchlistExpiryJob();
}
@ -2751,7 +2930,7 @@ class EditPage implements IEditObject {
$username = explode( '/', $this->mTitle->getText(), 2 )[0];
// Allow IP users
$validation = UserRigorOptions::RIGOR_NONE;
$user = MediaWikiServices::getInstance()->getUserFactory()->newFromName( $username, $validation );
$user = $this->userFactory->newFromName( $username, $validation );
$ip = $this->userNameUtils->isIP( $username );
$block = DatabaseBlock::newFromTarget( $user, $user );
@ -3363,7 +3542,8 @@ class EditPage implements IEditObject {
);
$out->wrapWikiMsg(
"<div id='mw-anon-edit-warning' class='mw-message-box mw-message-box-warning'>\n$1\n</div>",
[ 'anoneditwarning',
[
$this->tempUserCreateActive ? 'autocreate-edit-warning' : 'anoneditwarning',
// Log-in link
SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
'returnto' => $this->getTitle()->getPrefixedDBkey(),
@ -3379,7 +3559,7 @@ class EditPage implements IEditObject {
} else {
$out->wrapWikiMsg(
"<div id='mw-anon-preview-warning' class='mw-message-box mw-message-box-warning'>\n$1</div>",
'anonpreviewwarning'
$this->tempUserCreateActive ? 'autocreate-preview-warning' : 'anonpreviewwarning'
);
}
} elseif ( $this->mTitle->isUserConfigPage() ) {
@ -3724,7 +3904,7 @@ class EditPage implements IEditObject {
if ( $newContent ) {
$this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
$user = $this->context->getUser();
$user = $this->getUserForPreview();
$popts = ParserOptions::newFromUserAndLang( $user,
MediaWikiServices::getInstance()->getContentLanguage() );
$services = MediaWikiServices::getInstance();
@ -4262,7 +4442,7 @@ class EditPage implements IEditObject {
* - html: The HTML to be displayed
*/
protected function doPreviewParse( Content $content ) {
$user = $this->context->getUser();
$user = $this->getUserForPreview();
$parserOptions = $this->getPreviewParserOptions();
// NOTE: preSaveTransform doesn't have a fake revision to operate on.

View file

@ -1281,7 +1281,8 @@ return [
$services->getTitleFormatter(),
$services->getHttpRequestFactory(),
$services->getTrackingCategories(),
$services->getSignatureValidatorFactory()
$services->getSignatureValidatorFactory(),
$services->getUserNameUtils()
);
},
@ -1939,6 +1940,7 @@ return [
$services->getGroupPermissionsLookup(),
$services->getJobQueueGroupFactory(),
LoggerFactory::getInstance( 'UserGroupManager' ),
$services->getTempUserConfig(),
[ static function ( UserIdentity $user ) use ( $services ) {
$services->getPermissionManager()->invalidateUsersRightsCache( $user );
$services->getUserFactory()->newFromUserIdentity( $user )->invalidateCache();

View file

@ -165,6 +165,12 @@ class PageUpdater {
*/
private $forceEmptyRevision = false;
/**
* @var bool Whether to prevent new revision creation by throwing if it is
* attempted.
*/
private $preventChange = false;
/**
* @var array
*/
@ -334,6 +340,22 @@ class PageUpdater {
return User::newFromIdentity( $user );
}
/**
* After creation of the user during the save process, update the stored
* UserIdentity.
* @since 1.39
*
* @param UserIdentity $author
*/
public function updateAuthor( UserIdentity $author ) {
if ( $this->author->getName() !== $author->getName() ) {
throw new \MWException( 'Cannot replace the author with an author ' .
'of a different name, since DerivedPageDataUpdater may have stored the ' .
'old name.' );
}
$this->author = $author;
}
/**
* Can be used to enable or disable automatic summaries that are applied to certain kinds of
* changes, like completely blanking a page.
@ -1073,6 +1095,25 @@ class PageUpdater {
return !$this->wasRevisionCreated();
}
/**
* Whether the prepared edit is a change compared to the previous revision.
*
* @return bool
*/
public function isChange() {
return $this->derivedDataUpdater->isChange();
}
/**
* Disable new revision creation, throwing an exception if it is attempted.
*
* @return $this
*/
public function preventChange() {
$this->preventChange = true;
return $this;
}
/**
* Whether saveRevision() did create a revision. This is not the same as wasSuccessful():
* when the new content is exactly the same as the old one (DerivedPageDataUpdater::isChange()
@ -1291,10 +1332,17 @@ class PageUpdater {
$changed = $this->derivedDataUpdater->isChange();
if ( $this->forceEmptyRevision && $changed ) {
throw new LogicException(
'Content was changed even though forceEmptyRevision() was called.'
);
if ( $changed ) {
if ( $this->forceEmptyRevision ) {
throw new LogicException(
"Content was changed even though forceEmptyRevision() was called."
);
}
if ( $this->preventChange ) {
throw new LogicException(
"Content was changed even though preventChange() was called."
);
}
}
// We build the EditResult before the $change if/else branch in order to pass
@ -1423,6 +1471,11 @@ class PageUpdater {
* @return Status
*/
private function doCreate( CommentStoreComment $summary ) {
if ( $this->preventChange ) {
throw new LogicException(
"Content was changed even though preventChange() was called."
);
}
$wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
if ( !$this->derivedDataUpdater->getSlots()->hasSlot( SlotRecord::MAIN ) ) {

View file

@ -109,6 +109,8 @@ class McrUndoAction extends FormAction {
[ 'readonlywarning', $this->readOnlyMode->getReason() ]
);
} elseif ( $this->context->getUser()->isAnon() ) {
// Note: EditPage has a special message for temp user creation intent here.
// But McrUndoAction doesn't support user creation.
if ( !$this->getRequest()->getCheck( 'wpPreview' ) ) {
$out->wrapWikiMsg(
"<div id='mw-anon-edit-warning' class='mw-message-box mw-message-box-warning'>\n$1\n</div>",

View file

@ -622,17 +622,22 @@ class ChangeTags {
* @param string[] $tags Tags that you are interested in applying
* @param Authority|null $performer whose permission you wish to check, or null to
* check for a generic non-blocked user with the relevant rights
* @param bool $checkBlock Whether to check the blocked status of $performer
* @return Status
* @since 1.25
*/
public static function canAddTagsAccompanyingChange( array $tags, Authority $performer = null ) {
public static function canAddTagsAccompanyingChange(
array $tags,
Authority $performer = null,
$checkBlock = true
) {
$user = null;
if ( $performer !== null ) {
if ( !$performer->isAllowed( 'applychangetags' ) ) {
return Status::newFatal( 'tags-apply-no-permission' );
}
if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
return Status::newFatal(
'tags-apply-blocked',
$performer->getUser()->getName()

View file

@ -64,7 +64,8 @@ class ChangeTagsConstraint implements IEditConstraint {
// service as part of T245964
$changeTagStatus = ChangeTags::canAddTagsAccompanyingChange(
$this->tags,
$this->performer
$this->performer,
false
);
if ( $changeTagStatus->isOK() ) {

View file

@ -45,6 +45,7 @@ use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\Tidy\TidyDriverBase;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserOptionsLookup;
use Psr\Log\LoggerInterface;
use Wikimedia\IPUtils;
@ -389,6 +390,9 @@ class Parser {
/** @var SignatureValidatorFactory */
private $signatureValidatorFactory;
/** @var UserNameUtils */
private $userNameUtils;
/**
* @internal For use by ServiceWiring
*/
@ -441,6 +445,7 @@ class Parser {
* @param HttpRequestFactory $httpRequestFactory
* @param TrackingCategories $trackingCategories
* @param SignatureValidatorFactory $signatureValidatorFactory
* @param UserNameUtils $userNameUtils
*/
public function __construct(
ServiceOptions $svcOptions,
@ -462,7 +467,8 @@ class Parser {
TitleFormatter $titleFormatter,
HttpRequestFactory $httpRequestFactory,
TrackingCategories $trackingCategories,
SignatureValidatorFactory $signatureValidatorFactory
SignatureValidatorFactory $signatureValidatorFactory,
UserNameUtils $userNameUtils
) {
if ( ParserFactory::$inParserFactory === 0 ) {
// Direct construction of Parser was deprecated in 1.34 and
@ -528,6 +534,7 @@ class Parser {
$this->httpRequestFactory = $httpRequestFactory;
$this->trackingCategories = $trackingCategories;
$this->signatureValidatorFactory = $signatureValidatorFactory;
$this->userNameUtils = $userNameUtils;
// These steps used to be done in "::firstCallInit()"
// (if you're chasing a reference from some old code)
@ -4733,7 +4740,13 @@ class Parser {
# If we're still here, make it a link to the user page
$userText = wfEscapeWikiText( $username );
$nickText = wfEscapeWikiText( $nickname );
$msgName = $user->isRegistered() ? 'signature' : 'signature-anon';
if ( $this->userNameUtils->isTemp( $username ) ) {
$msgName = 'signature-temp';
} elseif ( $user->isRegistered() ) {
$msgName = 'signature';
} else {
$msgName = 'signature-anon';
}
return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
->page( $this->getPage() )->text();

View file

@ -29,6 +29,7 @@ use MediaWiki\Preferences\SignatureValidatorFactory;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\Tidy\TidyDriverBase;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserOptionsLookup;
use Psr\Log\LoggerInterface;
@ -84,6 +85,9 @@ class ParserFactory {
/** @var SignatureValidatorFactory */
private $signatureValidatorFactory;
/** @var UserNameUtils */
private $userNameUtils;
/**
* Track calls to Parser constructor to aid in deprecation of direct
* Parser invocation. This is temporary: it will be removed once the
@ -123,6 +127,7 @@ class ParserFactory {
* @param HttpRequestFactory $httpRequestFactory
* @param TrackingCategories $trackingCategories
* @param SignatureValidatorFactory $signatureValidatorFactory
* @param UserNameUtils $userNameUtils
* @since 1.32
* @internal
*/
@ -145,7 +150,8 @@ class ParserFactory {
TitleFormatter $titleFormatter,
HttpRequestFactory $httpRequestFactory,
TrackingCategories $trackingCategories,
SignatureValidatorFactory $signatureValidatorFactory
SignatureValidatorFactory $signatureValidatorFactory,
UserNameUtils $userNameUtils
) {
$svcOptions->assertRequiredOptions( Parser::CONSTRUCTOR_OPTIONS );
@ -170,6 +176,7 @@ class ParserFactory {
$this->httpRequestFactory = $httpRequestFactory;
$this->trackingCategories = $trackingCategories;
$this->signatureValidatorFactory = $signatureValidatorFactory;
$this->userNameUtils = $userNameUtils;
}
/**
@ -201,7 +208,8 @@ class ParserFactory {
$this->titleFormatter,
$this->httpRequestFactory,
$this->trackingCategories,
$this->signatureValidatorFactory
$this->signatureValidatorFactory,
$this->userNameUtils
);
} finally {
self::$inParserFactory--;

View file

@ -32,6 +32,7 @@ use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Permissions\Authority;
use MediaWiki\Permissions\GroupPermissionsLookup;
use MediaWiki\User\TempUser\TempUserConfig;
use Psr\Log\LoggerInterface;
use ReadOnlyMode;
use Sanitizer;
@ -101,6 +102,9 @@ class UserGroupManager implements IDBAccessObject {
/** @var LoggerInterface */
private $logger;
/** @var TempUserConfig */
private $tempUserConfig;
/** @var callable[] */
private $clearCacheCallbacks;
@ -154,6 +158,7 @@ class UserGroupManager implements IDBAccessObject {
* @param GroupPermissionsLookup $groupPermissionsLookup
* @param JobQueueGroup $jobQueueGroup for this $dbDomain
* @param LoggerInterface $logger
* @param TempUserConfig $tempUserConfig
* @param callable[] $clearCacheCallbacks
* @param string|bool $dbDomain
*/
@ -166,6 +171,7 @@ class UserGroupManager implements IDBAccessObject {
GroupPermissionsLookup $groupPermissionsLookup,
JobQueueGroup $jobQueueGroup,
LoggerInterface $logger,
TempUserConfig $tempUserConfig,
array $clearCacheCallbacks = [],
$dbDomain = false
) {
@ -179,6 +185,7 @@ class UserGroupManager implements IDBAccessObject {
$this->groupPermissionsLookup = $groupPermissionsLookup;
$this->jobQueueGroup = $jobQueueGroup;
$this->logger = $logger;
$this->tempUserConfig = $tempUserConfig;
// Can't just inject ROM since we LB can be for foreign wiki
$this->readOnlyMode = new ReadOnlyMode( $configuredReadOnlyMode, $this->loadBalancer );
$this->clearCacheCallbacks = $clearCacheCallbacks;
@ -280,9 +287,11 @@ class UserGroupManager implements IDBAccessObject {
!$this->canUseCachedValues( $user, self::CACHE_IMPLICIT, $queryFlags )
) {
$groups = [ '*' ];
if ( $user->isRegistered() ) {
if ( $this->tempUserConfig->isReservedName( $user->getName() ) ) {
$groups[] = 'user';
} elseif ( $user->isRegistered() ) {
$groups[] = 'user';
$groups[] = 'named';
$groups = array_unique( array_merge(
$groups,
$this->getUserAutopromoteGroups( $user )

View file

@ -25,6 +25,7 @@ use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\JobQueue\JobQueueGroupFactory;
use MediaWiki\Permissions\GroupPermissionsLookup;
use MediaWiki\User\TempUser\TempUserConfig;
use Psr\Log\LoggerInterface;
use Wikimedia\Rdbms\ILBFactory;
@ -62,6 +63,9 @@ class UserGroupManagerFactory {
/** @var HookContainer */
private $hookContainer;
/** @var TempUserConfig */
private $tempUserConfig;
/**
* @param ServiceOptions $options
* @param ConfiguredReadOnlyMode $configuredReadOnlyMode
@ -71,6 +75,7 @@ class UserGroupManagerFactory {
* @param GroupPermissionsLookup $groupPermissionsLookup
* @param JobQueueGroupFactory $jobQueueGroupFactory
* @param LoggerInterface $logger
* @param TempUserConfig $tempUserConfig Assumed to be the same across all domains.
* @param callable[] $clearCacheCallbacks
*/
public function __construct(
@ -82,6 +87,7 @@ class UserGroupManagerFactory {
GroupPermissionsLookup $groupPermissionsLookup,
JobQueueGroupFactory $jobQueueGroupFactory,
LoggerInterface $logger,
TempUserConfig $tempUserConfig,
array $clearCacheCallbacks = []
) {
$this->options = $options;
@ -92,6 +98,7 @@ class UserGroupManagerFactory {
$this->groupPermissionLookup = $groupPermissionsLookup;
$this->jobQueueGroupFactory = $jobQueueGroupFactory;
$this->logger = $logger;
$this->tempUserConfig = $tempUserConfig;
$this->clearCacheCallbacks = $clearCacheCallbacks;
}
@ -110,6 +117,7 @@ class UserGroupManagerFactory {
$this->groupPermissionLookup,
$this->jobQueueGroupFactory->makeJobQueueGroup( $dbDomain ),
$this->logger,
$this->tempUserConfig,
$this->clearCacheCallbacks,
$dbDomain
);

View file

@ -656,7 +656,9 @@
"showdiff": "Show changes",
"blankarticle": "<strong>Warning:</strong> The page you are creating is blank.\nIf you click \"$1\" again, the page will be created without any content.",
"anoneditwarning": "<strong>Warning:</strong> You are not logged in. Your IP address will be publicly visible if you make any edits. If you <strong>[$1 log in]</strong> or <strong>[$2 create an account]</strong>, your edits will be attributed to your username, along with other benefits.",
"autocreate-edit-warning": "<strong>Warning:</strong> You are not logged in. Your edit will be attributed to an <strong>auto-generated name</strong> by adding a cookie to your browser. Your IP address will be visible to trusted users. If you <strong>[$1 log in]</strong> or <strong>[$2 create an account]</strong>, your edits will be attributed to a name you choose, along with other benefits.",
"anonpreviewwarning": "<em>You are not logged in. Publishing will record your IP address in this page's edit history.</em>",
"autocreate-preview-warning": "<em>You are not logged in. Your edit will be attributed to an auto-generated name and your IP address will be visible to administrators.</em>",
"missingsummary": "<strong>Reminder:</strong> You have not provided an edit summary.\nIf you click \"$1\" again, your edit will be published without one.",
"selfredirect": "<strong>Warning:</strong> You are redirecting this page to itself.\nYou may have specified the wrong target for the redirect, or you may be editing the wrong page.\nIf you click \"$1\" again, the redirect will be created anyway.",
"missingcommenttext": "Please enter a comment.",
@ -3646,6 +3648,7 @@
"hebrew-calendar-m12-gen": "Elul",
"signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|talk]])",
"signature-anon": "[[{{#special:Contributions}}/$1|$2]]",
"signature-temp": "[[{{#special:Contributions}}/$1|$2]] ([[{{ns:user_talk}}:$1|talk]])",
"timezone-utc": "UTC",
"timezone-local": "Local",
"duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",

View file

@ -890,7 +890,9 @@
"showdiff": "Button below the edit page. See also {{msg-mw|Showpreview}} and {{msg-mw|Savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showdiff}}\n* {{msg-mw|Accesskey-diff}}\n* {{msg-mw|Tooltip-diff}}\n{{Identical|Show change}}",
"blankarticle": "Notice displayed once after the user tries to save an empty page.\n\nParameters:\n* $1 The label of the save button one of {{msg-mw|savearticle}} or {{msg-mw|savechanges}} on save-labelled wiki, or {{msg-mw|publishpage}} or {{msg-mw|publishchanges}} on publish-labelled wikis.",
"anoneditwarning": "Shown when editing a page anonymously.\n\nParameters:\n* $1 A link to log in, <nowiki>{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}</nowiki>\n* $2 A link to sign up, <nowiki>{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}</nowiki>\n\nSee also:\n* {{msg-mw|Mobile-frontend-editor-anonwarning}}",
"autocreate-edit-warning": "Shown when editing a page anonymously when temporary user auto-creation is enabled.\n\nParameters:\n* $1 A link to log in, <nowiki>{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}</nowiki>\n* $2 A link to sign up, <nowiki>{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}</nowiki>\n\nSee also:\n* {{msg-mw|Mobile-frontend-editor-anonwarning}}",
"anonpreviewwarning": "See also:\n* {{msg-mw|Anoneditwarning}}",
"autocreate-preview-warning": "Shown when previewing a page anonymously, when temporary user auto-creation is enabled. See also:\n* {{msg-mw|Autocreate-edit-warning}}",
"missingsummary": "The text \"edit summary\" is in {{msg-mw|Summary}}.\n\nSee also:\n* {{msg-mw|Missingcommentheader}}\n* {{msg-mw|Savearticle}}\n\nParameters:\n* $1 The label of the save button one of {{msg-mw|savearticle}} or {{msg-mw|savechanges}} on save-labelled wiki, or {{msg-mw|publishpage}} or {{msg-mw|publishchanges}} on publish-labelled wikis.",
"selfredirect": "Notice displayed once after the user tries to create a redirect to the same article.\n\nParameters:\n* $1 The label of the save button one of {{msg-mw|savearticle}} or {{msg-mw|savechanges}} on save-labelled wiki, or {{msg-mw|publishpage}} or {{msg-mw|publishchanges}} on publish-labelled wikis.",
"missingcommenttext": "This message is shown when the user tries to save a textbox created by the new section links, and the textbox is empty. \"Comment\" refers to the content that is supposed to be posted in the new section, usually a talk page comment.",
@ -3880,6 +3882,7 @@
"hebrew-calendar-m12-gen": "{{optional}}\nName of month in Hebrew calendar.",
"signature": "This will be substituted in the signature (~<nowiki></nowiki>~~ or ~~<nowiki></nowiki>~~ excluding timestamp).\n\nTranslate the word \"talk\" towards the end.\n\nParameters:\n* $1 - the username that is currently login\n* $2 - the customized signature which is specified in [[Special:Preferences|user's preferences]] as non-raw\n\nUse your language default parentheses ({{msg-mw|parentheses}}), but not use the message direct.\n\nSee also:\n* {{msg-mw|Signature-anon}} - signature for anonymous user",
"signature-anon": "{{notranslate}}\nUsed as signature for anonymous user. Parameters:\n* $1 - username (IP address?)\n* $2 - nickname (IP address?)\nSee also:\n* {{msg-mw|Signature}} - signature for registered user",
"signature-temp": "{{notranslate}}\nUsed as a signature for automatically created temporary users (when IP masking is enabled).",
"timezone-utc": "{{optional}}",
"timezone-local": "Label to indicate that a time is in the user's local timezone.\n{{Identical|Local}}",
"duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",

View file

@ -1043,4 +1043,77 @@ class PageUpdaterTest extends MediaWikiIntegrationTestCase {
$this->assertSame( $page->getCurrentUpdate(), $updater->prepareUpdate() );
}
/**
* @covers \MediaWiki\Storage\PageUpdater::preventChange
* @covers \MediaWiki\Storage\PageUpdater::doModify
* @covers \MediaWiki\Storage\PageUpdater::isChange
*/
public function testPreventChange_modify() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
// Creation
$summary = CommentStoreComment::newUnsavedComment( 'one' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
$updater->prepareUpdate();
$rev = $updater->saveRevision( $summary, EDIT_NEW );
$this->assertInstanceOf( RevisionRecord::class, $rev );
// Null edit
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'one' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
$updater->prepareUpdate();
$updater->preventChange();
$this->assertFalse( $updater->isChange() );
$rev = $updater->saveRevision( $summary );
$this->assertNull( $rev );
// Prevented edit
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'one' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
$updater->prepareUpdate();
$updater->preventChange();
$this->expectException( LogicException::class );
$updater->saveRevision( $summary );
}
/**
* @covers \MediaWiki\Storage\PageUpdater::preventChange
* @covers \MediaWiki\Storage\PageUpdater::doCreate
* @covers \MediaWiki\Storage\PageUpdater::isChange
*/
public function testPreventChange_create() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'one' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
$updater->prepareUpdate();
$updater->preventChange();
$this->assertTrue( $updater->isChange() );
$this->expectException( LogicException::class );
$updater->saveRevision( $summary, EDIT_NEW );
}
public function testUpdateAuthor() {
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$user = new User;
$user->setName( 'PageUpdaterTest' );
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'one' );
$updater->setContent( SlotRecord::MAIN, new TextContent( '~~~~' ) );
$user = User::createNew( $user->getName() );
$updater->updateAuthor( $user );
$rev = $updater->saveRevision( $summary, EDIT_NEW );
$this->assertGreaterThan( 0, $rev->getUser()->getId() );
}
}

View file

@ -5,6 +5,7 @@ use MediaWiki\Http\HttpRequestFactory;
use MediaWiki\Page\PageReference;
use MediaWiki\Page\PageReferenceValue;
use MediaWiki\Preferences\SignatureValidatorFactory;
use MediaWiki\User\UserNameUtils;
/**
* @covers Parser::__construct
@ -61,7 +62,8 @@ class ParserTest extends MediaWikiIntegrationTestCase {
$this->createMock( TitleFormatter::class ),
$this->createMock( HttpRequestFactory::class ),
$this->createMock( TrackingCategories::class ),
$this->createMock( SignatureValidatorFactory::class )
$this->createMock( SignatureValidatorFactory::class ),
$this->createMock( UserNameUtils::class )
];
}

View file

@ -27,6 +27,7 @@ use MediaWiki\Config\ServiceOptions;
use MediaWiki\Permissions\SimpleAuthority;
use MediaWiki\Session\PHPSessionHandler;
use MediaWiki\Session\SessionManager;
use MediaWiki\User\TempUser\RealTempUserConfig;
use MediaWiki\User\UserEditTracker;
use MediaWiki\User\UserGroupManager;
use MediaWiki\User\UserIdentity;
@ -94,6 +95,14 @@ class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
$services->getGroupPermissionsLookup(),
$services->getJobQueueGroup(),
new TestLogger(),
new RealTempUserConfig( [
'enabled' => true,
'actions' => [ 'edit' ],
'serialProvider' => [ 'type' => 'local' ],
'serialMapping' => [ 'type' => 'plain-numeric' ],
'matchPattern' => '*Unregistered $1',
'genPattern' => '*Unregistered $1'
] ),
$callback ? [ $callback ] : []
);
}
@ -163,13 +172,13 @@ class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
$manager = $this->getManager();
$user = $this->getTestUser( 'unittesters' )->getUser();
$this->assertArrayEquals(
[ '*', 'user', 'autoconfirmed' ],
[ '*', 'user', 'named', 'autoconfirmed' ],
$manager->getUserImplicitGroups( $user )
);
$user = $this->getTestUser( [ 'bureaucrat', 'test' ] )->getUser();
$this->assertArrayEquals(
[ '*', 'user', 'autoconfirmed' ],
[ '*', 'user', 'named', 'autoconfirmed' ],
$manager->getUserImplicitGroups( $user )
);
@ -178,7 +187,7 @@ class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
'added user to group'
);
$this->assertArrayEquals(
[ '*', 'user', 'autoconfirmed' ],
[ '*', 'user', 'named', 'autoconfirmed' ],
$manager->getUserImplicitGroups( $user )
);
@ -190,35 +199,42 @@ class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
] ] );
$user = $this->getTestUser()->getUser();
$this->assertArrayEquals(
[ '*', 'user' ],
[ '*', 'user', 'named' ],
$manager->getUserImplicitGroups( $user )
);
$this->assertArrayEquals(
[ '*', 'user' ],
[ '*', 'user', 'named' ],
$manager->getUserEffectiveGroups( $user )
);
$user->confirmEmail();
$this->assertArrayEquals(
[ '*', 'user', 'dummy' ],
[ '*', 'user', 'named', 'dummy' ],
$manager->getUserImplicitGroups( $user, UserGroupManager::READ_NORMAL, true )
);
$this->assertArrayEquals(
[ '*', 'user', 'dummy' ],
[ '*', 'user', 'named', 'dummy' ],
$manager->getUserEffectiveGroups( $user )
);
$user = $this->getTestUser( [ 'dummy' ] )->getUser();
$user->confirmEmail();
$this->assertArrayEquals(
[ '*', 'user', 'dummy' ],
[ '*', 'user', 'named', 'dummy' ],
$manager->getUserImplicitGroups( $user )
);
$user = new User;
$user->setName( '*Unregistered 1234' );
$this->assertArrayEquals(
[ '*', 'user' ],
$manager->getUserImplicitGroups( $user )
);
}
public function provideGetEffectiveGroups() {
yield [ [], [ '*', 'user', 'autoconfirmed' ] ];
yield [ [ 'bureaucrat', 'test' ], [ '*', 'user', 'autoconfirmed', 'bureaucrat', 'test' ] ];
yield [ [ 'autoconfirmed', 'test' ], [ '*', 'user', 'autoconfirmed', 'test' ] ];
yield [ [], [ '*', 'user', 'named', 'autoconfirmed' ] ];
yield [ [ 'bureaucrat', 'test' ], [ '*', 'user', 'named', 'autoconfirmed', 'bureaucrat', 'test' ] ];
yield [ [ 'autoconfirmed', 'test' ], [ '*', 'user', 'named', 'autoconfirmed', 'test' ] ];
}
/**

View file

@ -9,6 +9,7 @@ use MediaWiki\Preferences\SignatureValidatorFactory;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\Tidy\TidyDriverBase;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserOptionsLookup;
use Wikimedia\TestingAccessWrapper;
@ -66,7 +67,8 @@ class ParserFactoryTest extends MediaWikiUnitTestCase {
$this->createNoOpMock( TitleFormatter::class ),
$this->createNoOpMock( HttpRequestFactory::class ),
$this->createNoOpMock( TrackingCategories::class ),
$this->createNoOpMock( SignatureValidatorFactory::class )
$this->createNoOpMock( SignatureValidatorFactory::class ),
$this->createNoOpMock( UserNameUtils::class )
);
return $factory;
}