Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.97% covered (success)
93.97%
109 / 116
76.19% covered (warning)
76.19%
16 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
TempUserCreator
93.97% covered (success)
93.97%
109 / 116
76.19% covered (warning)
76.19%
16 / 21
39.33
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
 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
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
4.07
 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 ExtensionRegistry;
6use MediaWiki\Auth\AuthManager;
7use MediaWiki\Auth\Throttler;
8use MediaWiki\Permissions\Authority;
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        'plain-numeric' => [
54            'class' => PlainNumericSerialMapping::class,
55        ],
56        'localized-numeric' => [
57            'class' => LocalizedNumericSerialMapping::class,
58            'services' => [ 'LanguageFactory' ],
59        ],
60        'filtered-radix' => [
61            'class' => FilteredRadixSerialMapping::class,
62        ],
63        'scramble' => [
64            'class' => ScrambleMapping::class,
65        ]
66    ];
67
68    public function __construct(
69        RealTempUserConfig $config,
70        ObjectFactory $objectFactory,
71        UserFactory $userFactory,
72        AuthManager $authManager,
73        CentralIdLookup $centralIdLookup,
74        Throttler $tempAccountCreationThrottler,
75        Throttler $tempAccountNameAcquisitionThrottler
76    ) {
77        $this->config = $config;
78        $this->objectFactory = $objectFactory;
79        $this->userFactory = $userFactory;
80        $this->authManager = $authManager;
81        $this->centralIdLookup = $centralIdLookup;
82        $this->tempAccountCreationThrottler = $tempAccountCreationThrottler;
83        $this->tempAccountNameAcquisitionThrottler = $tempAccountNameAcquisitionThrottler;
84        $this->serialProviderConfig = $config->getSerialProviderConfig();
85        $this->serialMappingConfig = $config->getSerialMappingConfig();
86    }
87
88    /**
89     * Acquire a serial number, create the corresponding user and log in.
90     *
91     * @param string|null $name Previously acquired name
92     * @param WebRequest $request Request details, used for throttling
93     * @return CreateStatus
94     */
95    public function create( ?string $name, WebRequest $request ): CreateStatus {
96        $status = new CreateStatus;
97
98        // Check name acquisition rate limits first.
99        if ( $name === null ) {
100            $name = $this->acquireName( $request->getIP() );
101            if ( $name === null ) {
102                // If the $name remains null after calling ::acquireName, then
103                // we cannot generate a username and therefore cannot create a user.
104                // This could also happen if acquiring the name was rate limited
105                // In this case return a CreateStatus indicating no user was created.
106                // TODO: Create a custom message to support workflows related to T357802
107                return CreateStatus::newFatal( 'temp-user-unable-to-acquire' );
108            }
109        }
110
111        // Check temp account creation rate limits.
112        // TODO: This is duplicated from ThrottlePreAuthenticationProvider
113        // and should be factored out, see T261744
114        $result = $this->tempAccountCreationThrottler->increase(
115            null, $request->getIP(), 'TempUserCreator' );
116        if ( $result ) {
117            // TODO: Use a custom message here (T357777, T357802)
118            $message = wfMessage( 'acct_creation_throttle_hit' )->params( $result['count'] )
119                ->durationParams( $result['wait'] );
120            $status->fatal( $message );
121            return $status;
122        }
123
124        $createStatus = $this->attemptAutoCreate( $name );
125
126        if ( $createStatus->isOK() ) {
127            // The temporary account name didn't already exist, so now attempt to login
128            // using ::attemptAutoCreate as there isn't a public method to just login.
129            $this->attemptAutoCreate( $name, true );
130        }
131        return $createStatus;
132    }
133
134    public function isEnabled() {
135        return $this->config->isEnabled();
136    }
137
138    public function isAutoCreateAction( string $action ) {
139        return $this->config->isAutoCreateAction( $action );
140    }
141
142    public function shouldAutoCreate( Authority $authority, string $action ) {
143        return $this->config->shouldAutoCreate( $authority, $action );
144    }
145
146    public function isTempName( string $name ) {
147        return $this->config->isTempName( $name );
148    }
149
150    public function isReservedName( string $name ) {
151        return $this->config->isReservedName( $name );
152    }
153
154    public function getPlaceholderName(): string {
155        return $this->config->getPlaceholderName();
156    }
157
158    public function getMatchPattern(): Pattern {
159        return $this->config->getMatchPattern();
160    }
161
162    public function getMatchPatterns(): array {
163        return $this->config->getMatchPatterns();
164    }
165
166    public function getMatchCondition( IReadableDatabase $db, string $field, string $op ): IExpression {
167        return $this->config->getMatchCondition( $db, $field, $op );
168    }
169
170    public function getExpireAfterDays(): ?int {
171        return $this->config->getExpireAfterDays();
172    }
173
174    public function getNotifyBeforeExpirationDays(): ?int {
175        return $this->config->getNotifyBeforeExpirationDays();
176    }
177
178    /**
179     * Attempts to auto create a temporary user using
180     * AuthManager::autoCreateUser, and optionally log them
181     * in if $login is true.
182     *
183     * @param string $name
184     * @param bool $login Whether to also log the user in to this temporary account.
185     * @return CreateStatus
186     */
187    private function attemptAutoCreate( string $name, bool $login = false ): CreateStatus {
188        $createStatus = new CreateStatus;
189        // Verify the $name is usable.
190        $user = $this->userFactory->newFromName( $name, UserRigorOptions::RIGOR_USABLE );
191        if ( !$user ) {
192            $createStatus->fatal( 'internalerror_info',
193                'Unable to create user with automatically generated name' );
194            return $createStatus;
195        }
196        $status = $this->authManager->autoCreateUser(
197            $user,
198            AuthManager::AUTOCREATE_SOURCE_TEMP,
199            $login, // login
200            false // log
201        );
202        $createStatus->merge( $status );
203        // If a userexists warning is a part of the status, then
204        // add the fatal error temp-user-unable-to-acquire.
205        if ( $createStatus->hasMessage( 'userexists' ) ) {
206            $createStatus->fatal( 'temp-user-unable-to-acquire' );
207        }
208        if ( $createStatus->isOK() ) {
209            $createStatus->value = $user;
210        }
211        return $createStatus;
212    }
213
214    /**
215     * Acquire a new username and return it. Permanently reserve the ID in
216     * the database.
217     *
218     * @param string $ip The IP address associated with this name acquisition request.
219     * @return string|null The username, or null if the auto-generated username is
220     *    already in use, or if the attempt trips the TempAccountNameAcquisitionThrottle limits.
221     */
222    private function acquireName( string $ip ): ?string {
223        if ( $this->tempAccountNameAcquisitionThrottler->increase(
224            null, $ip, 'TempUserCreator'
225        ) ) {
226            return null;
227        }
228        $year = null;
229        if ( $this->serialProviderConfig['useYear'] ?? false ) {
230            $year = MWTimestamp::getInstance()->format( 'Y' );
231        }
232        // Check if the temporary account name is already in use as the ID provided
233        // may not be properly collision safe (T353390)
234        $index = $this->getSerialProvider()->acquireIndex( (int)$year );
235        $serialId = $this->getSerialMapping()->getSerialIdForIndex( $index );
236        $username = $this->config->getGeneratorPattern()->generate( $serialId, $year );
237
238        // Because the ::acquireIndex method may not always return a unique index,
239        // make sure that the temporary account name does not already exist. This
240        // is needed because of the problems discussed in T353390.
241        // The problems discussed at that task should not require the use of a primary lookup.
242        $centralId = $this->centralIdLookup->centralIdFromName(
243            $username,
244            CentralIdLookup::AUDIENCE_RAW
245        );
246        if ( !$centralId ) {
247            // If no user exists with this name centrally, then return the $username.
248            return $username;
249        }
250        return null;
251    }
252
253    /**
254     * Get the serial provider
255     * @return SerialProvider
256     */
257    private function getSerialProvider(): SerialProvider {
258        if ( !isset( $this->serialProvider ) ) {
259            $this->serialProvider = $this->createSerialProvider();
260        }
261        return $this->serialProvider;
262    }
263
264    /**
265     * Create the serial provider
266     * @return SerialProvider
267     */
268    private function createSerialProvider(): SerialProvider {
269        $type = $this->serialProviderConfig['type'];
270        if ( isset( self::SERIAL_PROVIDERS[$type] ) ) {
271            $spec = self::SERIAL_PROVIDERS[$type];
272        } else {
273            $extensionProviders = ExtensionRegistry::getInstance()
274                ->getAttribute( 'TempUserSerialProviders' );
275            if ( isset( $extensionProviders[$type] ) ) {
276                $spec = $extensionProviders[$type];
277            } else {
278                throw new UnexpectedValueException( __CLASS__ . ": unknown serial provider \"$type\"" );
279            }
280        }
281
282        /** @noinspection PhpIncompatibleReturnTypeInspection */
283        // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
284        return $this->objectFactory->createObject(
285            $spec,
286            [
287                'assertClass' => SerialProvider::class,
288                'extraArgs' => [ $this->serialProviderConfig ]
289            ]
290        );
291    }
292
293    /**
294     * Get the serial mapping
295     * @return SerialMapping
296     */
297    private function getSerialMapping(): SerialMapping {
298        if ( !isset( $this->serialMapping ) ) {
299            $this->serialMapping = $this->createSerialMapping();
300        }
301        return $this->serialMapping;
302    }
303
304    /**
305     * Create the serial map
306     * @return SerialMapping
307     */
308    private function createSerialMapping(): SerialMapping {
309        $type = $this->serialMappingConfig['type'];
310        if ( isset( self::SERIAL_MAPPINGS[$type] ) ) {
311            $spec = self::SERIAL_MAPPINGS[$type];
312        } else {
313            $extensionMappings = ExtensionRegistry::getInstance()
314                ->getAttribute( 'TempUserSerialMappings' );
315            if ( isset( $extensionMappings[$type] ) ) {
316                $spec = $extensionMappings[$type];
317            } else {
318                throw new UnexpectedValueException( __CLASS__ . ": unknown serial mapping \"$type\"" );
319            }
320        }
321        /** @noinspection PhpIncompatibleReturnTypeInspection */
322        // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
323        return $this->objectFactory->createObject(
324            $spec,
325            [
326                'assertClass' => SerialMapping::class,
327                'extraArgs' => [ $this->serialMappingConfig ]
328            ]
329        );
330    }
331
332    /**
333     * Permanently acquire a username, stash it in a session, and return it.
334     * Do not create the user.
335     *
336     * If this method was called before with the same session ID, return the
337     * previously stashed username instead of acquiring a new one.
338     *
339     * @param Session $session
340     * @return string|null The username, or null if no username could be acquired
341     */
342    public function acquireAndStashName( Session $session ) {
343        $name = $session->get( 'TempUser:name' );
344        if ( $name !== null ) {
345            return $name;
346        }
347        $name = $this->acquireName( $session->getRequest()->getIP() );
348        if ( $name !== null ) {
349            $session->set( 'TempUser:name', $name );
350            $session->save();
351        }
352        return $name;
353    }
354
355    /**
356     * Return a possible acquired and stashed username in a session.
357     * Do not acquire or create the user.
358     *
359     * If this method is called with the same session ID as function acquireAndStashName(),
360     * it returns the previously stashed username.
361     *
362     * @since 1.41
363     * @param Session $session
364     * @return ?string The username, if it was already acquired
365     */
366    public function getStashedName( Session $session ): ?string {
367        return $session->get( 'TempUser:name' );
368    }
369}