Split concepts of "match pattern" and "reserved pattern". Add config "reservedPattern" which specifies a username pattern to be denied for creation, but unlike matchPattern, it does not immediately flag all accounts with such names as temp accounts. This can be used before migration or after rollback to safely prevent creation of conflicting accounts. * Add TempUserConfig::isTempName(), which does what isReservedName() previously did, i.e. determines if a name is a temp username. * Make TempUserConfig::isReservedName() return true if the name matches either the match pattern or the reserved pattern. * UserNameUtils::isTemp() continues to determine whether a username is actually a temp username. * Add UserNameUtils::isTempReserved() as a proxy for TempUserConfig::isReservedName() Bug: T307064 Change-Id: I26755731fad89530a06824181106cd7b4e077d9e
408 lines
11 KiB
PHP
408 lines
11 KiB
PHP
<?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
|
|
* @author DannyS712
|
|
*/
|
|
|
|
namespace MediaWiki\User;
|
|
|
|
use InvalidArgumentException;
|
|
use Language;
|
|
use MalformedTitleException;
|
|
use MediaWiki\Config\ServiceOptions;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\HookRunner;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\User\TempUser\TempUserConfig;
|
|
use Psr\Log\LoggerInterface;
|
|
use TitleParser;
|
|
use Wikimedia\IPUtils;
|
|
use Wikimedia\Message\ITextFormatter;
|
|
use Wikimedia\Message\MessageValue;
|
|
|
|
/**
|
|
* UserNameUtils service
|
|
*
|
|
* @since 1.35
|
|
*/
|
|
class UserNameUtils implements UserRigorOptions {
|
|
|
|
/**
|
|
* @internal For use by ServiceWiring
|
|
*/
|
|
public const CONSTRUCTOR_OPTIONS = [
|
|
MainConfigNames::MaxNameChars,
|
|
MainConfigNames::ReservedUsernames,
|
|
MainConfigNames::InvalidUsernameCharacters
|
|
];
|
|
|
|
/**
|
|
* RIGOR_* constants are inherited from UserRigorOptions
|
|
*/
|
|
|
|
/**
|
|
* @var ServiceOptions
|
|
*/
|
|
private $options;
|
|
|
|
/**
|
|
* @var Language
|
|
*/
|
|
private $contentLang;
|
|
|
|
/**
|
|
* @var LoggerInterface
|
|
*/
|
|
private $logger;
|
|
|
|
/**
|
|
* @var TitleParser
|
|
*/
|
|
private $titleParser;
|
|
|
|
/**
|
|
* @var ITextFormatter
|
|
*/
|
|
private $textFormatter;
|
|
|
|
/**
|
|
* @var string[]|false Cache for isUsable()
|
|
*/
|
|
private $reservedUsernames = false;
|
|
|
|
/**
|
|
* @var HookRunner
|
|
*/
|
|
private $hookRunner;
|
|
|
|
/** @var TempUserConfig */
|
|
private $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, 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 )
|
|
|| 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;
|
|
}
|
|
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:
|
|
if ( !$this->isValid( $name ) ) {
|
|
return false;
|
|
}
|
|
return $name;
|
|
case self::RIGOR_USABLE:
|
|
if ( !$this->isUsable( $name ) ) {
|
|
return false;
|
|
}
|
|
return $name;
|
|
case self::RIGOR_CREATABLE:
|
|
if ( !$this->isCreatable( $name ) ) {
|
|
return false;
|
|
}
|
|
return $name;
|
|
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 = '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/';
|
|
$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 );
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|