3 namespace MediaWiki\User\TempUser
;
5 use MediaWiki\Auth\AuthManager
;
6 use MediaWiki\Auth\Throttler
;
7 use MediaWiki\Permissions\Authority
;
8 use MediaWiki\Registration\ExtensionRegistry
;
9 use MediaWiki\Request\WebRequest
;
10 use MediaWiki\Session\Session
;
11 use MediaWiki\User\CentralId\CentralIdLookup
;
12 use MediaWiki\User\UserFactory
;
13 use MediaWiki\User\UserRigorOptions
;
14 use MediaWiki\Utils\MWTimestamp
;
15 use UnexpectedValueException
;
16 use Wikimedia\ObjectFactory\ObjectFactory
;
17 use Wikimedia\Rdbms\IExpression
;
18 use Wikimedia\Rdbms\IReadableDatabase
;
21 * Service for temporary user creation. For convenience this also proxies the
22 * TempUserConfig methods.
24 * This is separate from TempUserConfig to avoid dependency loops. Special pages
25 * and actions are free to use this class, but services should take it as a
26 * constructor parameter only if necessary.
30 class TempUserCreator
implements TempUserConfig
{
31 private RealTempUserConfig
$config;
32 private UserFactory
$userFactory;
33 private AuthManager
$authManager;
34 private CentralIdLookup
$centralIdLookup;
35 private Throttler
$tempAccountCreationThrottler;
36 private Throttler
$tempAccountNameAcquisitionThrottler;
37 private array $serialProviderConfig;
38 private array $serialMappingConfig;
39 private ObjectFactory
$objectFactory;
40 private ?SerialProvider
$serialProvider;
41 private ?SerialMapping
$serialMapping;
43 /** ObjectFactory specs for the core serial providers */
44 private const SERIAL_PROVIDERS
= [
46 'class' => LocalSerialProvider
::class,
47 'services' => [ 'DBLoadBalancerFactory' ],
51 /** ObjectFactory specs for the core serial maps */
52 private const SERIAL_MAPPINGS
= [
53 'readable-numeric' => [
54 'class' => ReadableNumericSerialMapping
::class,
57 'class' => PlainNumericSerialMapping
::class,
59 'localized-numeric' => [
60 'class' => LocalizedNumericSerialMapping
::class,
61 'services' => [ 'LanguageFactory' ],
64 'class' => FilteredRadixSerialMapping
::class,
67 'class' => ScrambleMapping
::class,
71 public function __construct(
72 RealTempUserConfig
$config,
73 ObjectFactory
$objectFactory,
74 UserFactory
$userFactory,
75 AuthManager
$authManager,
76 CentralIdLookup
$centralIdLookup,
77 Throttler
$tempAccountCreationThrottler,
78 Throttler
$tempAccountNameAcquisitionThrottler
80 $this->config
= $config;
81 $this->objectFactory
= $objectFactory;
82 $this->userFactory
= $userFactory;
83 $this->authManager
= $authManager;
84 $this->centralIdLookup
= $centralIdLookup;
85 $this->tempAccountCreationThrottler
= $tempAccountCreationThrottler;
86 $this->tempAccountNameAcquisitionThrottler
= $tempAccountNameAcquisitionThrottler;
87 $this->serialProviderConfig
= $config->getSerialProviderConfig();
88 $this->serialMappingConfig
= $config->getSerialMappingConfig();
92 * Acquire a serial number, create the corresponding user and log in.
94 * @param string|null $name Previously acquired name
95 * @param WebRequest $request Request details, used for throttling
96 * @return CreateStatus
98 public function create( ?
string $name, WebRequest
$request ): CreateStatus
{
99 $status = new CreateStatus
;
101 // Check name acquisition rate limits first.
102 if ( $name === null ) {
103 $name = $this->acquireName( $request->getIP() );
104 if ( $name === null ) {
105 // If the $name remains null after calling ::acquireName, then
106 // we cannot generate a username and therefore cannot create a user.
107 // This could also happen if acquiring the name was rate limited
108 // In this case return a CreateStatus indicating no user was created.
109 // TODO: Create a custom message to support workflows related to T357802
110 return CreateStatus
::newFatal( 'temp-user-unable-to-acquire' );
114 // Check temp account creation rate limits.
115 // TODO: This is duplicated from ThrottlePreAuthenticationProvider
116 // and should be factored out, see T261744
117 $result = $this->tempAccountCreationThrottler
->increase(
118 null, $request->getIP(), 'TempUserCreator' );
120 // TODO: Use a custom message here (T357777, T357802)
121 $message = wfMessage( 'acct_creation_throttle_hit' )->params( $result['count'] )
122 ->durationParams( $result['wait'] );
123 $status->fatal( $message );
127 $createStatus = $this->attemptAutoCreate( $name );
129 if ( $createStatus->isOK() ) {
130 // The temporary account name didn't already exist, so now attempt to login
131 // using ::attemptAutoCreate as there isn't a public method to just login.
132 $this->attemptAutoCreate( $name, true );
134 return $createStatus;
137 public function isEnabled() {
138 return $this->config
->isEnabled();
141 public function isKnown() {
142 return $this->config
->isKnown();
145 public function isAutoCreateAction( string $action ) {
146 return $this->config
->isAutoCreateAction( $action );
149 public function shouldAutoCreate( Authority
$authority, string $action ) {
150 return $this->config
->shouldAutoCreate( $authority, $action );
153 public function isTempName( string $name ) {
154 return $this->config
->isTempName( $name );
157 public function isReservedName( string $name ) {
158 return $this->config
->isReservedName( $name );
161 public function getPlaceholderName(): string {
162 return $this->config
->getPlaceholderName();
165 public function getMatchPattern(): Pattern
{
166 return $this->config
->getMatchPattern();
169 public function getMatchPatterns(): array {
170 return $this->config
->getMatchPatterns();
173 public function getMatchCondition( IReadableDatabase
$db, string $field, string $op ): IExpression
{
174 return $this->config
->getMatchCondition( $db, $field, $op );
177 public function getExpireAfterDays(): ?
int {
178 return $this->config
->getExpireAfterDays();
181 public function getNotifyBeforeExpirationDays(): ?
int {
182 return $this->config
->getNotifyBeforeExpirationDays();
186 * Attempts to auto create a temporary user using
187 * AuthManager::autoCreateUser, and optionally log them
188 * in if $login is true.
190 * @param string $name
191 * @param bool $login Whether to also log the user in to this temporary account.
192 * @return CreateStatus
194 private function attemptAutoCreate( string $name, bool $login = false ): CreateStatus
{
195 $createStatus = new CreateStatus
;
196 // Verify the $name is usable.
197 $user = $this->userFactory
->newFromName( $name, UserRigorOptions
::RIGOR_USABLE
);
199 $createStatus->fatal( 'internalerror_info',
200 'Unable to create user with automatically generated name' );
201 return $createStatus;
203 $status = $this->authManager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_TEMP
, $login );
204 $createStatus->merge( $status );
205 // If a userexists warning is a part of the status, then
206 // add the fatal error temp-user-unable-to-acquire.
207 if ( $createStatus->hasMessage( 'userexists' ) ) {
208 $createStatus->fatal( 'temp-user-unable-to-acquire' );
210 if ( $createStatus->isOK() ) {
211 $createStatus->value
= $user;
213 return $createStatus;
217 * Acquire a new username and return it. Permanently reserve the ID in
220 * @param string $ip The IP address associated with this name acquisition request.
221 * @return string|null The username, or null if the auto-generated username is
222 * already in use, or if the attempt trips the TempAccountNameAcquisitionThrottle limits.
224 private function acquireName( string $ip ): ?
string {
225 if ( $this->tempAccountNameAcquisitionThrottler
->increase(
226 null, $ip, 'TempUserCreator'
231 if ( $this->serialProviderConfig
['useYear'] ??
false ) {
232 $year = MWTimestamp
::getInstance()->format( 'Y' );
234 // Check if the temporary account name is already in use as the ID provided
235 // may not be properly collision safe (T353390)
236 $index = $this->getSerialProvider()->acquireIndex( (int)$year );
237 $serialId = $this->getSerialMapping()->getSerialIdForIndex( $index );
238 $username = $this->config
->getGeneratorPattern()->generate( $serialId, $year );
240 // Because the ::acquireIndex method may not always return a unique index,
241 // make sure that the temporary account name does not already exist. This
242 // is needed because of the problems discussed in T353390.
243 // The problems discussed at that task should not require the use of a primary lookup.
244 $centralId = $this->centralIdLookup
->centralIdFromName(
246 CentralIdLookup
::AUDIENCE_RAW
249 // If no user exists with this name centrally, then return the $username.
256 * Get the serial provider
257 * @return SerialProvider
259 private function getSerialProvider(): SerialProvider
{
260 if ( !isset( $this->serialProvider
) ) {
261 $this->serialProvider
= $this->createSerialProvider();
263 return $this->serialProvider
;
267 * Create the serial provider
268 * @return SerialProvider
270 private function createSerialProvider(): SerialProvider
{
271 $type = $this->serialProviderConfig
['type'];
272 if ( isset( self
::SERIAL_PROVIDERS
[$type] ) ) {
273 $spec = self
::SERIAL_PROVIDERS
[$type];
275 $extensionProviders = ExtensionRegistry
::getInstance()
276 ->getAttribute( 'TempUserSerialProviders' );
277 if ( isset( $extensionProviders[$type] ) ) {
278 $spec = $extensionProviders[$type];
280 throw new UnexpectedValueException( __CLASS__
. ": unknown serial provider \"$type\"" );
284 /** @noinspection PhpIncompatibleReturnTypeInspection */
285 // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
286 return $this->objectFactory
->createObject(
289 'assertClass' => SerialProvider
::class,
290 'extraArgs' => [ $this->serialProviderConfig
]
296 * Get the serial mapping
297 * @return SerialMapping
299 private function getSerialMapping(): SerialMapping
{
300 if ( !isset( $this->serialMapping
) ) {
301 $this->serialMapping
= $this->createSerialMapping();
303 return $this->serialMapping
;
307 * Create the serial map
308 * @return SerialMapping
310 private function createSerialMapping(): SerialMapping
{
311 $type = $this->serialMappingConfig
['type'];
312 if ( isset( self
::SERIAL_MAPPINGS
[$type] ) ) {
313 $spec = self
::SERIAL_MAPPINGS
[$type];
315 $extensionMappings = ExtensionRegistry
::getInstance()
316 ->getAttribute( 'TempUserSerialMappings' );
317 if ( isset( $extensionMappings[$type] ) ) {
318 $spec = $extensionMappings[$type];
320 throw new UnexpectedValueException( __CLASS__
. ": unknown serial mapping \"$type\"" );
323 /** @noinspection PhpIncompatibleReturnTypeInspection */
324 // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
325 return $this->objectFactory
->createObject(
328 'assertClass' => SerialMapping
::class,
329 'extraArgs' => [ $this->serialMappingConfig
]
335 * Permanently acquire a username, stash it in a session, and return it.
336 * Do not create the user.
338 * If this method was called before with the same session ID, return the
339 * previously stashed username instead of acquiring a new one.
341 * @param Session $session
342 * @return string|null The username, or null if no username could be acquired
344 public function acquireAndStashName( Session
$session ) {
345 $name = $session->get( 'TempUser:name' );
346 if ( $name !== null ) {
349 $name = $this->acquireName( $session->getRequest()->getIP() );
350 if ( $name !== null ) {
351 $session->set( 'TempUser:name', $name );
358 * Return a possible acquired and stashed username in a session.
359 * Do not acquire or create the user.
361 * If this method is called with the same session ID as function acquireAndStashName(),
362 * it returns the previously stashed username.
365 * @param Session $session
366 * @return ?string The username, if it was already acquired
368 public function getStashedName( Session
$session ): ?
string {
369 return $session->get( 'TempUser:name' );