1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
|
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\User;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Language\Language;
use MediaWiki\MainConfigNames;
use MediaWiki\Title\MalformedTitleException;
use MediaWiki\Title\TitleParser;
use MediaWiki\User\TempUser\TempUserConfig;
use Psr\Log\LoggerInterface;
use Wikimedia\IPUtils;
use Wikimedia\Message\ITextFormatter;
use Wikimedia\Message\MessageValue;
/**
* UserNameUtils service
*
* @since 1.35
* @ingroup User
* @author DannyS712
*/
class UserNameUtils implements UserRigorOptions {
/**
* @internal For use by ServiceWiring
*/
public const CONSTRUCTOR_OPTIONS = [
MainConfigNames::MaxNameChars,
MainConfigNames::ReservedUsernames,
MainConfigNames::InvalidUsernameCharacters
];
/**
* For use by isIP() and isLikeIPv4DashRange()
*/
private const IPV4_ADDRESS = '\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})';
// RIGOR_* constants are inherited from UserRigorOptions
// phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.WrongStyle
private ServiceOptions $options;
private Language $contentLang;
private LoggerInterface $logger;
private TitleParser $titleParser;
private ITextFormatter $textFormatter;
/**
* @var string[]|false Cache for isUsable()
*/
private $reservedUsernames = false;
private HookRunner $hookRunner;
private TempUserConfig $tempUserConfig;
/**
* @param ServiceOptions $options
* @param Language $contentLang
* @param LoggerInterface $logger
* @param TitleParser $titleParser
* @param ITextFormatter $textFormatter the text formatter for the current content language
* @param HookContainer $hookContainer
* @param TempUserConfig $tempUserConfig
*/
public function __construct(
ServiceOptions $options,
Language $contentLang,
LoggerInterface $logger,
TitleParser $titleParser,
ITextFormatter $textFormatter,
HookContainer $hookContainer,
TempUserConfig $tempUserConfig
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->contentLang = $contentLang;
$this->logger = $logger;
$this->titleParser = $titleParser;
$this->textFormatter = $textFormatter;
$this->hookRunner = new HookRunner( $hookContainer );
$this->tempUserConfig = $tempUserConfig;
}
/**
* Is the input a valid username?
*
* Checks if the input is a valid username, we don't want an empty string,
* an IP address, any type of IP range, anything that contains slashes
* (would mess up subpages), is longer than the maximum allowed username
* size or doesn't begin with a capital letter.
*
* @param string $name Name to match
* @return bool
*/
public function isValid( string $name ): bool {
if ( $name === ''
|| $this->isIP( $name )
|| $this->isValidIPRange( $name )
|| $this->isLikeIPv4DashRange( $name )
|| str_contains( $name, '/' )
|| strlen( $name ) > $this->options->get( MainConfigNames::MaxNameChars )
|| $name !== $this->contentLang->ucfirst( $name )
) {
return false;
}
// Ensure that the name can't be misresolved as a different title,
// such as with extra namespace keys at the start.
try {
$title = $this->titleParser->parseTitle( $name );
} catch ( MalformedTitleException $_ ) {
$title = null;
}
if ( $title === null
|| $title->getNamespace()
|| strcmp( $name, $title->getText() )
) {
return false;
}
// Check an additional list of troublemaker characters.
// Should these be merged into the title char list?
$unicodeList = '/[' .
'\x{0080}-\x{009f}' . # iso-8859-1 control chars
'\x{00a0}' . # non-breaking space
'\x{2000}-\x{200f}' . # various whitespace
'\x{2028}-\x{202f}' . # breaks and control chars
'\x{3000}' . # ideographic space
'\x{e000}-\x{f8ff}' . # private use
']/u';
if ( preg_match( $unicodeList, $name ) ) {
return false;
}
return true;
}
/**
* Usernames which fail to pass this function will be blocked
* from user login and new account registrations, but may be used
* internally by batch processes.
*
* If an account already exists in this form, login will be blocked
* by a failure to pass this function.
*
* @param string $name Name to match
* @return bool
*/
public function isUsable( string $name ): bool {
// Must be a valid username, obviously ;)
if ( !$this->isValid( $name ) ) {
return false;
}
if ( !$this->reservedUsernames ) {
$reservedUsernames = $this->options->get( MainConfigNames::ReservedUsernames );
$this->hookRunner->onUserGetReservedNames( $reservedUsernames );
foreach ( $reservedUsernames as &$reserved ) {
if ( str_starts_with( $reserved, 'msg:' ) ) {
$reserved = $this->textFormatter->format(
MessageValue::new( substr( $reserved, 4 ) )
);
}
}
$this->reservedUsernames = $reservedUsernames;
}
// Certain names may be reserved for batch processes.
if ( in_array( $name, $this->reservedUsernames, true ) ) {
return false;
}
// Treat this name as not usable if it is reserved by the temp user system and either:
// * Temporary account creation is disabled
// * The name is not a temporary account
// This is necessary to ensure that CentralAuth auto-creation will be denied (T342475).
if (
$this->isTempReserved( $name ) &&
( !$this->tempUserConfig->isEnabled() || !$this->isTemp( $name ) )
) {
return false;
}
return true;
}
/**
* Usernames which fail to pass this function will be blocked
* from new account registrations, but may be used internally
* either by batch processes or by user accounts which have
* already been created.
*
* Additional preventions may be added here rather than in
* isValid() to avoid disrupting existing accounts.
*
* @param string $name String to match
* @return bool
*/
public function isCreatable( string $name ): bool {
// Ensure that the username isn't longer than 235 bytes, so that
// (at least for the builtin skins) user javascript and css files
// will work. (T25080)
if ( strlen( $name ) > 235 ) {
$this->logger->debug(
__METHOD__ . ": '$name' uncreatable due to length"
);
return false;
}
$invalid = $this->options->get( MainConfigNames::InvalidUsernameCharacters );
// Preg yells if you try to give it an empty string
if ( $invalid !== '' &&
preg_match( '/[' . preg_quote( $invalid, '/' ) . ']/', $name )
) {
$this->logger->debug(
__METHOD__ . ": '$name' uncreatable due to wgInvalidUsernameCharacters"
);
return false;
}
if ( $this->isTempReserved( $name ) ) {
$this->logger->debug(
__METHOD__ . ": '$name' uncreatable due to TempUserConfig"
);
return false;
}
return $this->isUsable( $name );
}
/**
* Given unvalidated user input, return a canonical username, or false if
* the username is invalid.
* @param string $name User input
* @param string $validate Type of validation to use
* Use of public constants RIGOR_* is preferred
* - RIGOR_NONE No validation
* - RIGOR_VALID Valid for batch processes
* - RIGOR_USABLE Valid for batch processes and login
* - RIGOR_CREATABLE Valid for batch processes, login and account creation
*
* @throws InvalidArgumentException
* @return string|false
*/
public function getCanonical( string $name, string $validate = self::RIGOR_VALID ) {
// Force usernames to capital
$name = $this->contentLang->ucfirst( $name );
// Reject names containing '#'; these will be cleaned up
// with title normalisation, but then it's too late to
// check elsewhere
if ( strpos( $name, '#' ) !== false ) {
return false;
}
// No need to proceed if no validation is requested, just
// clean up underscores and user namespace prefix (see T283915).
if ( $validate === self::RIGOR_NONE ) {
// This is only needed here because if validation is
// not self::RIGOR_NONE, it would be done at title parsing stage.
$nsPrefix = $this->contentLang->getNsText( NS_USER ) . ':';
if ( str_starts_with( $name, $nsPrefix ) ) {
$name = str_replace( $nsPrefix, '', $name );
}
$name = strtr( $name, '_', ' ' );
return $name;
}
// Clean up name according to title rules,
// but only when validation is requested (T14654)
try {
$title = $this->titleParser->parseTitle( $name, NS_USER );
} catch ( MalformedTitleException $_ ) {
$title = null;
}
// Check for invalid titles
if ( $title === null
|| $title->getNamespace() !== NS_USER
|| $title->isExternal()
) {
return false;
}
$name = $title->getText();
// RIGOR_NONE handled above
switch ( $validate ) {
case self::RIGOR_VALID:
return $this->isValid( $name ) ? $name : false;
case self::RIGOR_USABLE:
return $this->isUsable( $name ) ? $name : false;
case self::RIGOR_CREATABLE:
return $this->isCreatable( $name ) ? $name : false;
default:
throw new InvalidArgumentException(
"Invalid parameter value for validation ($validate) in " .
__METHOD__
);
}
}
/**
* Does the string match an anonymous IP address?
*
* This function exists for username validation, in order to reject
* usernames which are similar in form to IP addresses. Strings such
* as 300.300.300.300 will return true because it looks like an IP
* address, despite not being strictly valid.
*
* We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
* address because the usemod software would "cloak" anonymous IP
* addresses like this, if we allowed accounts like this to be created
* new users could get the old edits of these anonymous users.
*
* This does //not// match IPv6 ranges (T239527)
*
* @param string $name Name to check
* @return bool
*/
public function isIP( string $name ): bool {
$anyIPv4 = '/^' . self::IPV4_ADDRESS . '$/';
$validIP = IPUtils::isValid( $name );
return $validIP || preg_match( $anyIPv4, $name );
}
/**
* Wrapper for IPUtils::isValidRange
*
* @param string $range Range to check
* @return bool
*/
public function isValidIPRange( string $range ): bool {
return IPUtils::isValidRange( $range );
}
/**
* Validates IPv4 and IPv4-like ranges in the form of 1.2.3.4-5.6.7.8,
* (which we'd like to avoid as a username/title pattern).
*
* @since 1.42
* @param string $range IPv4 dash range to check
* @return bool
*/
public function isLikeIPv4DashRange( string $range ): bool {
return preg_match(
'/^' . self::IPV4_ADDRESS . '-' . self::IPV4_ADDRESS . '$/',
$range
);
}
/**
* Does the username indicate a temporary user?
*
* @since 1.39
* @param string $name
* @return bool
*/
public function isTemp( string $name ) {
return $this->tempUserConfig->isTempName( $name );
}
/**
* Is the username uncreatable due to it being reserved by the temp username
* system? Note that unlike isTemp(), this does not imply that a user having
* this name is an actual temp account. This should only be used to deny
* account creation.
*
* @since 1.41
* @param string $name
* @return bool
*/
public function isTempReserved( string $name ) {
return $this->tempUserConfig->isReservedName( $name );
}
/**
* Get a placeholder name for a temporary user before serial acquisition
*
* @since 1.39
* @return string
*/
public function getTempPlaceholder() {
return $this->tempUserConfig->getPlaceholderName();
}
}
|