Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.54% covered (success)
95.54%
107 / 112
77.27% covered (warning)
77.27%
17 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
TempUserCreator
95.54% covered (success)
95.54%
107 / 112
77.27% covered (warning)
77.27%
17 / 22
40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isKnown
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isAutoCreateAction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shouldAutoCreate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTempName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReservedName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPlaceholderName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatchPattern
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatchPatterns
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatchCondition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExpireAfterDays
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNotifyBeforeExpirationDays
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 attemptAutoCreate
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 acquireName
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 getSerialProvider
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 createSerialProvider
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 getSerialMapping
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 createSerialMapping
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 acquireAndStashName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getStashedName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\User\TempUser;
4
5use MediaWiki\Auth\AuthManager;
6use MediaWiki\Auth\Throttler;
7use MediaWiki\Permissions\Authority;
8use MediaWiki\Registration\ExtensionRegistry;
9use MediaWiki\Request\WebRequest;
10use MediaWiki\Session\Session;
11use MediaWiki\User\CentralId\CentralIdLookup;
12use MediaWiki\User\UserFactory;
13use MediaWiki\User\UserRigorOptions;
14use MediaWiki\Utils\MWTimestamp;
15use UnexpectedValueException;
16use Wikimedia\ObjectFactory\ObjectFactory;
17use Wikimedia\Rdbms\IExpression;
18use Wikimedia\Rdbms\IReadableDatabase;
19
20/**
21 * Service for temporary user creation. For convenience this also proxies the
22 * TempUserConfig methods.
23 *
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.
27 *
28 * @since 1.39
29 */
30class 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;
42
43    /** ObjectFactory specs for the core serial providers */
44    private const SERIAL_PROVIDERS = [
45        'local' => [
46            'class' => LocalSerialProvider::class,
47            'services' => [ 'DBLoadBalancer' ],
48        ]
49    ];
50
51    /** ObjectFactory specs for the core serial maps */
52    private const SERIAL_MAPPINGS = [
53        'readable-numeric' => [
54            'class' => ReadableNumericSerialMapping::class,
55        ],
56        'plain-numeric' => [
57            'class' => PlainNumericSerialMapping::class,
58        ],
59        'localized-numeric' => [
60            'class' => LocalizedNumericSerialMapping::class,
61            'services' => [ 'LanguageFactory' ],
62        ],
63        'filtered-radix' => [
64            'class' => FilteredRadixSerialMapping::class,
65        ],
66        'scramble' => [
67            'class' => ScrambleMapping::class,
68        ]
69    ];
70
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();
89    }
90
91    /**
92     * Acquire a serial number, create the corresponding user and log in.
93     *
94     * @param string|null $name Previously acquired name
95     * @param WebRequest $request Request details, used for throttling
96     * @return CreateStatus
97     */
98    public function create( ?string $name, WebRequest $request ): CreateStatus {
99        $status = new CreateStatus;
100
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' );
111            }
112        }
113
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;
125        }
126
127        $createStatus = $this->attemptAutoCreate( $name );
128
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 );
133        }
134        return $createStatus;
135    }
136
137    public function isEnabled() {
138        return $this->config->isEnabled();
139    }
140
141    public function isKnown() {
142        return $this->config->isKnown();
143    }
144
145    public function isAutoCreateAction( string $action ) {
146        return $this->config->isAutoCreateAction( $action );
147    }
148
149    public function shouldAutoCreate( Authority $authority, string $action ) {
150        return $this->config->shouldAutoCreate( $authority, $action );
151    }
152
153    public function isTempName( string $name ) {
154        return $this->config->isTempName( $name );
155    }
156
157    public function isReservedName( string $name ) {
158        return $this->config->isReservedName( $name );
159    }
160
161    public function getPlaceholderName(): string {
162        return $this->config->getPlaceholderName();
163    }
164
165    public function getMatchPattern(): Pattern {
166        return $this->config->getMatchPattern();
167    }
168
169    public function getMatchPatterns(): array {
170        return $this->config->getMatchPatterns();
171    }
172
173    public function getMatchCondition( IReadableDatabase $db, string $field, string $op ): IExpression {
174        return $this->config->getMatchCondition( $db, $field, $op );
175    }
176
177    public function getExpireAfterDays(): ?int {
178        return $this->config->getExpireAfterDays();
179    }
180
181    public function getNotifyBeforeExpirationDays(): ?int {
182        return $this->config->getNotifyBeforeExpirationDays();
183    }
184
185    /**
186     * Attempts to auto create a temporary user using
187     * AuthManager::autoCreateUser, and optionally log them
188     * in if $login is true.
189     *
190     * @param string $name
191     * @param bool $login Whether to also log the user in to this temporary account.
192     * @return CreateStatus
193     */
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;
202        }
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' );
209        }
210        if ( $createStatus->isOK() ) {
211            $createStatus->value = $user;
212        }
213        return $createStatus;
214    }
215
216    /**
217     * Acquire a new username and return it. Permanently reserve the ID in
218     * the database.
219     *
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.
223     */
224    private function acquireName( string $ip ): ?string {
225        if ( $this->tempAccountNameAcquisitionThrottler->increase(
226            null, $ip, 'TempUserCreator'
227        ) ) {
228            return null;
229        }
230        $year = null;
231        if ( $this->serialProviderConfig['useYear'] ?? false ) {
232            $year = MWTimestamp::getInstance()->format( 'Y' );
233        }
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 );
239
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
247        );
248        if ( !$centralId ) {
249            // If no user exists with this name centrally, then return the $username.
250            return $username;
251        }
252        return null;
253    }
254
255    /**
256     * Get the serial provider
257     * @return SerialProvider
258     */
259    private function getSerialProvider(): SerialProvider {
260        if ( !isset( $this->serialProvider ) ) {
261            $this->serialProvider = $this->createSerialProvider();
262        }
263        return $this->serialProvider;
264    }
265
266    /**
267     * Create the serial provider
268     * @return SerialProvider
269     */
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\"" );
281            }
282        }
283
284        /** @noinspection PhpIncompatibleReturnTypeInspection */
285        // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
286        return $this->objectFactory->createObject(
287            $spec,
288            [
289                'assertClass' => SerialProvider::class,
290                'extraArgs' => [ $this->serialProviderConfig ]
291            ]
292        );
293    }
294
295    /**
296     * Get the serial mapping
297     * @return SerialMapping
298     */
299    private function getSerialMapping(): SerialMapping {
300        if ( !isset( $this->serialMapping ) ) {
301            $this->serialMapping = $this->createSerialMapping();
302        }
303        return $this->serialMapping;
304    }
305
306    /**
307     * Create the serial map
308     * @return SerialMapping
309     */
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\"" );
321            }
322        }
323        /** @noinspection PhpIncompatibleReturnTypeInspection */
324        // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
325        return $this->objectFactory->createObject(
326            $spec,
327            [
328                'assertClass' => SerialMapping::class,
329                'extraArgs' => [ $this->serialMappingConfig ]
330            ]
331        );
332    }
333
334    /**
335     * Permanently acquire a username, stash it in a session, and return it.
336     * Do not create the user.
337     *
338     * If this method was called before with the same session ID, return the
339     * previously stashed username instead of acquiring a new one.
340     *
341     * @param Session $session
342     * @return string|null The username, or null if no username could be acquired
343     */
344    public function acquireAndStashName( Session $session ) {
345        $name = $session->get( 'TempUser:name' );
346        if ( $name !== null ) {
347            return $name;
348        }
349        $name = $this->acquireName( $session->getRequest()->getIP() );
350        if ( $name !== null ) {
351            $session->set( 'TempUser:name', $name );
352            $session->save();
353        }
354        return $name;
355    }
356
357    /**
358     * Return a possible acquired and stashed username in a session.
359     * Do not acquire or create the user.
360     *
361     * If this method is called with the same session ID as function acquireAndStashName(),
362     * it returns the previously stashed username.
363     *
364     * @since 1.41
365     * @param Session $session
366     * @return ?string The username, if it was already acquired
367     */
368    public function getStashedName( Session $session ): ?string {
369        return $session->get( 'TempUser:name' );
370    }
371}