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:
parent
6393713b44
commit
d6a3b6cfa8
16 changed files with 452 additions and 72 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 ) ) {
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() ) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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--;
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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\".",
|
||||
|
|
|
|||
|
|
@ -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\".",
|
||||
|
|
|
|||
|
|
@ -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() );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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' ] ];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue