Wrap libxml_disable_entity_loader() calls in version constraint
[mediawiki.git] / includes / user / UserNameUtils.php
blobfb17514fb08ea8f50dd01988e6a840268f3b6a22
1 <?php
3 /**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
19 * @file
20 * @author DannyS712
23 namespace MediaWiki\User;
25 use InvalidArgumentException;
26 use Language;
27 use MalformedTitleException;
28 use MediaWiki\Config\ServiceOptions;
29 use MediaWiki\HookContainer\HookContainer;
30 use MediaWiki\HookContainer\HookRunner;
31 use Psr\Log\LoggerInterface;
32 use TitleParser;
33 use Wikimedia\IPUtils;
34 use Wikimedia\Message\ITextFormatter;
35 use Wikimedia\Message\MessageValue;
37 /**
38 * UserNameUtils service
40 * @since 1.35
42 class UserNameUtils implements UserRigorOptions {
44 /**
45 * @internal For use by ServiceWiring
47 public const CONSTRUCTOR_OPTIONS = [
48 'MaxNameChars',
49 'ReservedUsernames',
50 'InvalidUsernameCharacters'
53 /**
54 * RIGOR_* constants are inherited from UserRigorOptions
57 /**
58 * @var ServiceOptions
60 private $options;
62 /**
63 * @var Language
65 private $contentLang;
67 /**
68 * @var LoggerInterface
70 private $logger;
72 /**
73 * @var TitleParser
75 private $titleParser;
77 /**
78 * @var ITextFormatter
80 private $textFormatter;
82 /**
83 * @var string[]|false Cache for isUsable()
85 private $reservedUsernames = false;
87 /**
88 * @var HookRunner
90 private $hookRunner;
92 /**
93 * @param ServiceOptions $options
94 * @param Language $contentLang
95 * @param LoggerInterface $logger
96 * @param TitleParser $titleParser
97 * @param ITextFormatter $textFormatter the text formatter for the current content language
98 * @param HookContainer $hookContainer
100 public function __construct(
101 ServiceOptions $options,
102 Language $contentLang,
103 LoggerInterface $logger,
104 TitleParser $titleParser,
105 ITextFormatter $textFormatter,
106 HookContainer $hookContainer
108 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
109 $this->options = $options;
110 $this->contentLang = $contentLang;
111 $this->logger = $logger;
112 $this->titleParser = $titleParser;
113 $this->textFormatter = $textFormatter;
114 $this->hookRunner = new HookRunner( $hookContainer );
118 * Is the input a valid username?
120 * Checks if the input is a valid username, we don't want an empty string,
121 * an IP address, anything that contains slashes (would mess up subpages),
122 * is longer than the maximum allowed username size or doesn't begin with
123 * a capital letter.
125 * @param string $name Name to match
126 * @return bool
128 public function isValid( string $name ) : bool {
129 if ( $name === ''
130 || $this->isIP( $name )
131 || strpos( $name, '/' ) !== false
132 || strlen( $name ) > $this->options->get( 'MaxNameChars' )
133 || $name !== $this->contentLang->ucfirst( $name )
135 return false;
138 // Ensure that the name can't be misresolved as a different title,
139 // such as with extra namespace keys at the start.
140 try {
141 $title = $this->titleParser->parseTitle( $name );
142 } catch ( MalformedTitleException $_ ) {
143 $title = null;
146 if ( $title === null
147 || $title->getNamespace()
148 || strcmp( $name, $title->getText() )
150 return false;
153 // Check an additional blacklist of troublemaker characters.
154 // Should these be merged into the title char list?
155 $unicodeBlacklist = '/[' .
156 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
157 '\x{00a0}' . # non-breaking space
158 '\x{2000}-\x{200f}' . # various whitespace
159 '\x{2028}-\x{202f}' . # breaks and control chars
160 '\x{3000}' . # ideographic space
161 '\x{e000}-\x{f8ff}' . # private use
162 ']/u';
163 if ( preg_match( $unicodeBlacklist, $name ) ) {
164 return false;
167 return true;
171 * Usernames which fail to pass this function will be blocked
172 * from user login and new account registrations, but may be used
173 * internally by batch processes.
175 * If an account already exists in this form, login will be blocked
176 * by a failure to pass this function.
178 * @param string $name Name to match
179 * @return bool
181 public function isUsable( string $name ) : bool {
182 // Must be a valid username, obviously ;)
183 if ( !$this->isValid( $name ) ) {
184 return false;
187 if ( !$this->reservedUsernames ) {
188 $reservedUsernames = $this->options->get( 'ReservedUsernames' );
189 $this->hookRunner->onUserGetReservedNames( $reservedUsernames );
190 $this->reservedUsernames = $reservedUsernames;
193 // Certain names may be reserved for batch processes.
194 foreach ( $this->reservedUsernames as $reserved ) {
195 if ( substr( $reserved, 0, 4 ) === 'msg:' ) {
196 $reserved = $this->textFormatter->format(
197 MessageValue::new( substr( $reserved, 4 ) )
200 if ( $reserved === $name ) {
201 return false;
204 return true;
208 * Usernames which fail to pass this function will be blocked
209 * from new account registrations, but may be used internally
210 * either by batch processes or by user accounts which have
211 * already been created.
213 * Additional blacklisting may be added here rather than in
214 * isValidUserName() to avoid disrupting existing accounts.
216 * @param string $name String to match
217 * @return bool
219 public function isCreatable( string $name ) : bool {
220 // Ensure that the username isn't longer than 235 bytes, so that
221 // (at least for the builtin skins) user javascript and css files
222 // will work. (T25080)
223 if ( strlen( $name ) > 235 ) {
224 $this->logger->debug(
225 __METHOD__ . ": '$name' uncreatable due to length"
227 return false;
230 $invalid = $this->options->get( 'InvalidUsernameCharacters' );
231 // Preg yells if you try to give it an empty string
232 if ( $invalid !== '' &&
233 preg_match( '/[' . preg_quote( $invalid, '/' ) . ']/', $name )
235 $this->logger->debug(
236 __METHOD__ . ": '$name' uncreatable due to wgInvalidUsernameCharacters"
238 return false;
241 return $this->isUsable( $name );
245 * Given unvalidated user input, return a canonical username, or false if
246 * the username is invalid.
247 * @param string $name User input
248 * @param string $validate Type of validation to use
249 * Use of public constants RIGOR_* is preferred
250 * - RIGOR_NONE No validation
251 * - RIGOR_VALID Valid for batch processes
252 * - RIGOR_USABLE Valid for batch processes and login
253 * - RIGOR_CREATABLE Valid for batch processes, login and account creation
255 * @throws InvalidArgumentException
256 * @return bool|string
258 public function getCanonical( string $name, string $validate = self::RIGOR_VALID ) {
259 // Force usernames to capital
260 $name = $this->contentLang->ucfirst( $name );
262 // Reject names containing '#'; these will be cleaned up
263 // with title normalisation, but then it's too late to
264 // check elsewhere
265 if ( strpos( $name, '#' ) !== false ) {
266 return false;
269 // No need to proceed if no validation is requested, just
270 // clean up underscores and return
271 if ( $validate === self::RIGOR_NONE ) {
272 $name = strtr( $name, '_', ' ' );
273 return $name;
276 // Clean up name according to title rules,
277 // but only when validation is requested (T14654)
278 try {
279 $title = $this->titleParser->parseTitle( $name, NS_USER );
280 } catch ( MalformedTitleException $_ ) {
281 $title = null;
284 // Check for invalid titles
285 if ( $title === null
286 || $title->getNamespace() !== NS_USER
287 || $title->isExternal()
289 return false;
292 $name = $title->getText();
294 // RIGOR_NONE handled above
295 switch ( $validate ) {
296 case self::RIGOR_VALID:
297 if ( !$this->isValid( $name ) ) {
298 return false;
300 return $name;
301 case self::RIGOR_USABLE:
302 if ( !$this->isUsable( $name ) ) {
303 return false;
305 return $name;
306 case self::RIGOR_CREATABLE:
307 if ( !$this->isCreatable( $name ) ) {
308 return false;
310 return $name;
311 default:
312 throw new InvalidArgumentException(
313 "Invalid parameter value for validation ($validate) in " .
314 __METHOD__
320 * Does the string match an anonymous IP address?
322 * This function exists for username validation, in order to reject
323 * usernames which are similar in form to IP addresses. Strings such
324 * as 300.300.300.300 will return true because it looks like an IP
325 * address, despite not being strictly valid.
327 * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
328 * address because the usemod software would "cloak" anonymous IP
329 * addresses like this, if we allowed accounts like this to be created
330 * new users could get the old edits of these anonymous users.
332 * Unlike User::isIP, this does //not// match IPv6 ranges (T239527)
334 * @param string $name Name to check
335 * @return bool
337 public function isIP( string $name ) : bool {
338 $anyIPv4 = '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/';
339 $validIP = IPUtils::isValid( $name );
340 return $validIP || preg_match( $anyIPv4, $name );
344 * Wrapper for IPUtils::isValidRange
346 * @param string $range Range to check
347 * @return bool
349 public function isValidIPRange( string $range ) : bool {
350 return IPUtils::isValidRange( $range );