Merge ".mailmap: Correct two contributor names"
[mediawiki.git] / includes / user / TempUser / TempUserCreator.php
blobaec44077fab3560b654786abb0e9cd4a169e908e
1 <?php
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;
20 /**
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.
28 * @since 1.39
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 = [
45 'local' => [
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,
56 'plain-numeric' => [
57 'class' => PlainNumericSerialMapping::class,
59 'localized-numeric' => [
60 'class' => LocalizedNumericSerialMapping::class,
61 'services' => [ 'LanguageFactory' ],
63 'filtered-radix' => [
64 'class' => FilteredRadixSerialMapping::class,
66 'scramble' => [
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
79 ) {
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();
91 /**
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' );
119 if ( $result ) {
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 );
124 return $status;
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 );
198 if ( !$user ) {
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
218 * the database.
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'
227 ) ) {
228 return null;
230 $year = null;
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(
245 $username,
246 CentralIdLookup::AUDIENCE_RAW
248 if ( !$centralId ) {
249 // If no user exists with this name centrally, then return the $username.
250 return $username;
252 return null;
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];
274 } else {
275 $extensionProviders = ExtensionRegistry::getInstance()
276 ->getAttribute( 'TempUserSerialProviders' );
277 if ( isset( $extensionProviders[$type] ) ) {
278 $spec = $extensionProviders[$type];
279 } else {
280 throw new UnexpectedValueException( __CLASS__ . ": unknown serial provider \"$type\"" );
284 /** @noinspection PhpIncompatibleReturnTypeInspection */
285 // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
286 return $this->objectFactory->createObject(
287 $spec,
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];
314 } else {
315 $extensionMappings = ExtensionRegistry::getInstance()
316 ->getAttribute( 'TempUserSerialMappings' );
317 if ( isset( $extensionMappings[$type] ) ) {
318 $spec = $extensionMappings[$type];
319 } else {
320 throw new UnexpectedValueException( __CLASS__ . ": unknown serial mapping \"$type\"" );
323 /** @noinspection PhpIncompatibleReturnTypeInspection */
324 // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
325 return $this->objectFactory->createObject(
326 $spec,
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 ) {
347 return $name;
349 $name = $this->acquireName( $session->getRequest()->getIP() );
350 if ( $name !== null ) {
351 $session->set( 'TempUser:name', $name );
352 $session->save();
354 return $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.
364 * @since 1.41
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' );