Merge "Add a new UserNameUtils service"
This commit is contained in:
commit
55d3efdb7c
6 changed files with 802 additions and 129 deletions
|
|
@ -59,6 +59,7 @@ use MediaWiki\Storage\BlobStoreFactory;
|
|||
use MediaWiki\Storage\NameTableStore;
|
||||
use MediaWiki\Storage\NameTableStoreFactory;
|
||||
use MediaWiki\Storage\PageEditStash;
|
||||
use MediaWiki\User\UserNameUtils;
|
||||
use MessageCache;
|
||||
use MimeAnalyzer;
|
||||
use MWException;
|
||||
|
|
@ -1187,6 +1188,14 @@ class MediaWikiServices extends ServiceContainer {
|
|||
return $this->getService( 'UploadRevisionImporter' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.35
|
||||
* @return UserNameUtils
|
||||
*/
|
||||
public function getUserNameUtils() : UserNameUtils {
|
||||
return $this->getService( 'UserNameUtils' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.28
|
||||
* @return VirtualRESTServiceClient
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ use MediaWiki\Storage\BlobStoreFactory;
|
|||
use MediaWiki\Storage\NameTableStoreFactory;
|
||||
use MediaWiki\Storage\PageEditStash;
|
||||
use MediaWiki\Storage\SqlBlobStore;
|
||||
use MediaWiki\User\UserNameUtils;
|
||||
use Wikimedia\DependencyStore\KeyValueDependencyStore;
|
||||
use Wikimedia\DependencyStore\SqlModuleDependencyStore;
|
||||
use Wikimedia\Message\IMessageFormatterFactory;
|
||||
|
|
@ -1056,6 +1057,19 @@ return [
|
|||
);
|
||||
},
|
||||
|
||||
'UserNameUtils' => function ( MediaWikiServices $services ) : UserNameUtils {
|
||||
// TODO there should be a proper injectable MessageLocalizer service (T247127)
|
||||
return new UserNameUtils(
|
||||
new ServiceOptions(
|
||||
UserNameUtils::CONSTRUCTOR_OPTIONS, $services->getMainConfig()
|
||||
),
|
||||
$services->getContentLanguage(),
|
||||
LoggerFactory::getInstance( 'UserNameUtils' ),
|
||||
$services->getService( 'TitleFactory' ),
|
||||
RequestContext::getMain()
|
||||
);
|
||||
},
|
||||
|
||||
'VirtualRESTServiceClient' =>
|
||||
function ( MediaWikiServices $services ) : VirtualRESTServiceClient {
|
||||
$config = $services->getMainConfig()->get( 'VirtualRestConfig' );
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ use MediaWiki\MediaWikiServices;
|
|||
use MediaWiki\Session\SessionManager;
|
||||
use MediaWiki\Session\Token;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
use MediaWiki\User\UserNameUtils;
|
||||
use Wikimedia\Assert\Assert;
|
||||
use Wikimedia\IPSet;
|
||||
use Wikimedia\IPUtils;
|
||||
|
|
@ -111,9 +112,6 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
'mActorId',
|
||||
];
|
||||
|
||||
/** @var string[]|false Cache for self::isUsableName() */
|
||||
private static $reservedUsernames = false;
|
||||
|
||||
/** Cache variables */
|
||||
// @{
|
||||
/** @var int */
|
||||
|
|
@ -942,6 +940,8 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
* addresses like this, if we allowed accounts like this to be created
|
||||
* new users could get the old edits of these anonymous users.
|
||||
*
|
||||
* @deprecated since 1.35, use the UserNameUtils service
|
||||
* Note that UserNameUtils::isIP does not accept IPv6 ranges, while this method does
|
||||
* @param string $name Name to match
|
||||
* @return bool
|
||||
*/
|
||||
|
|
@ -953,6 +953,7 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
/**
|
||||
* Is the user an IP range?
|
||||
*
|
||||
* @deprecated since 1.35, use the UserNameUtils service or IPUtils directly
|
||||
* @since 1.30
|
||||
* @return bool
|
||||
*/
|
||||
|
|
@ -968,45 +969,12 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
* is longer than the maximum allowed username size or doesn't begin with
|
||||
* a capital letter.
|
||||
*
|
||||
* @deprecated since 1.35, use the UserNameUtils service
|
||||
* @param string $name Name to match
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidUserName( $name ) {
|
||||
global $wgMaxNameChars;
|
||||
|
||||
if ( $name == ''
|
||||
|| self::isIP( $name )
|
||||
|| strpos( $name, '/' ) !== false
|
||||
|| strlen( $name ) > $wgMaxNameChars
|
||||
|| $name != MediaWikiServices::getInstance()->getContentLanguage()->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.
|
||||
$parsed = Title::newFromText( $name );
|
||||
if ( $parsed === null
|
||||
|| $parsed->getNamespace()
|
||||
|| strcmp( $name, $parsed->getPrefixedText() ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check an additional blacklist of troublemaker characters.
|
||||
// Should these be merged into the title char list?
|
||||
$unicodeBlacklist = '/[' .
|
||||
'\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( $unicodeBlacklist, $name ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return MediaWikiServices::getInstance()->getUserNameUtils()->isValid( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1017,31 +985,12 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
* If an account already exists in this form, login will be blocked
|
||||
* by a failure to pass this function.
|
||||
*
|
||||
* @deprecated since 1.35, use the UserNameUtils service
|
||||
* @param string $name Name to match
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsableName( $name ) {
|
||||
global $wgReservedUsernames;
|
||||
// Must be a valid username, obviously ;)
|
||||
if ( !self::isValidUserName( $name ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !self::$reservedUsernames ) {
|
||||
self::$reservedUsernames = $wgReservedUsernames;
|
||||
Hooks::run( 'UserGetReservedNames', [ &self::$reservedUsernames ] );
|
||||
}
|
||||
|
||||
// Certain names may be reserved for batch processes.
|
||||
foreach ( self::$reservedUsernames as $reserved ) {
|
||||
if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
|
||||
$reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->plain();
|
||||
}
|
||||
if ( $reserved == $name ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return MediaWikiServices::getInstance()->getUserNameUtils()->isUsable( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1091,31 +1040,12 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
* Additional blacklisting may be added here rather than in
|
||||
* isValidUserName() to avoid disrupting existing accounts.
|
||||
*
|
||||
* @deprecated since 1.35, use the UserNameUtils service
|
||||
* @param string $name String to match
|
||||
* @return bool
|
||||
*/
|
||||
public static function isCreatableName( $name ) {
|
||||
global $wgInvalidUsernameCharacters;
|
||||
|
||||
// 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 ) {
|
||||
wfDebugLog( 'username', __METHOD__ .
|
||||
": '$name' invalid due to length" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Preg yells if you try to give it an empty string
|
||||
if ( $wgInvalidUsernameCharacters !== '' &&
|
||||
preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name )
|
||||
) {
|
||||
wfDebugLog( 'username', __METHOD__ .
|
||||
": '$name' invalid due to wgInvalidUsernameCharacters" );
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isUsableName( $name );
|
||||
return MediaWikiServices::getInstance()->getUserNameUtils()->isCreatable( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1182,6 +1112,8 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
/**
|
||||
* Given unvalidated user input, return a canonical username, or false if
|
||||
* the username is invalid.
|
||||
*
|
||||
* @deprecated since 1.35, use the UserNameUtils service
|
||||
* @param string $name User input
|
||||
* @param string|bool $validate Type of validation to use:
|
||||
* - false No validation
|
||||
|
|
@ -1193,50 +1125,26 @@ class User implements IDBAccessObject, UserIdentity {
|
|||
* @return bool|string
|
||||
*/
|
||||
public static function getCanonicalName( $name, $validate = 'valid' ) {
|
||||
// Force usernames to capital
|
||||
$name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
|
||||
// Backwards compatibility with strings / false
|
||||
$validationLevels = [
|
||||
'valid' => UserNameUtils::RIGOR_VALID,
|
||||
'usable' => UserNameUtils::RIGOR_USABLE,
|
||||
'creatable' => UserNameUtils::RIGOR_CREATABLE
|
||||
];
|
||||
|
||||
# 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;
|
||||
if ( $validate === false ) {
|
||||
$validation = UserNameUtils::RIGOR_NONE;
|
||||
} elseif ( array_key_exists( $validate, $validationLevels ) ) {
|
||||
$validation = $validationLevels[ $validate ];
|
||||
} else {
|
||||
// Not a recognized value, probably a test for unsupported validation
|
||||
// levels, regardless, just pass it along
|
||||
$validation = $validate;
|
||||
}
|
||||
|
||||
// Clean up name according to title rules,
|
||||
// but only when validation is requested (T14654)
|
||||
$t = ( $validate !== false ) ?
|
||||
Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name );
|
||||
// Check for invalid titles
|
||||
if ( $t === null || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = $t->getText();
|
||||
|
||||
switch ( $validate ) {
|
||||
case false:
|
||||
break;
|
||||
case 'valid':
|
||||
if ( !self::isValidUserName( $name ) ) {
|
||||
$name = false;
|
||||
}
|
||||
break;
|
||||
case 'usable':
|
||||
if ( !self::isUsableName( $name ) ) {
|
||||
$name = false;
|
||||
}
|
||||
break;
|
||||
case 'creatable':
|
||||
if ( !self::isCreatableName( $name ) ) {
|
||||
$name = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid parameter value for $validate in ' . __METHOD__ );
|
||||
}
|
||||
return $name;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getUserNameUtils()
|
||||
->getCanonical( (string)$name, $validation );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
331
includes/user/UserNameUtils.php
Normal file
331
includes/user/UserNameUtils.php
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
<?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 Hooks;
|
||||
use InvalidArgumentException;
|
||||
use Language;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MessageLocalizer;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use TitleFactory;
|
||||
use Wikimedia\IPUtils;
|
||||
|
||||
/**
|
||||
* UserNameUtils service
|
||||
*
|
||||
* @since 1.35
|
||||
*/
|
||||
class UserNameUtils {
|
||||
|
||||
public const CONSTRUCTOR_OPTIONS = [
|
||||
'MaxNameChars',
|
||||
'ReservedUsernames',
|
||||
'InvalidUsernameCharacters'
|
||||
];
|
||||
|
||||
public const RIGOR_CREATABLE = 'creatable';
|
||||
public const RIGOR_USABLE = 'usable';
|
||||
public const RIGOR_VALID = 'valid';
|
||||
public const RIGOR_NONE = 'none';
|
||||
|
||||
/**
|
||||
* @var ServiceOptions
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @var Language
|
||||
*/
|
||||
private $contentLang;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @var TitleFactory
|
||||
*/
|
||||
private $titleFactory;
|
||||
|
||||
/**
|
||||
* @var MessageLocalizer
|
||||
*/
|
||||
private $msgLocalizer;
|
||||
|
||||
/**
|
||||
* @var string[]|false Cache for isUsable()
|
||||
*/
|
||||
private $reservedUsernames = false;
|
||||
|
||||
/**
|
||||
* @param ServiceOptions $options
|
||||
* @param Language $contentLang
|
||||
* @param LoggerInterface $logger
|
||||
* @param TitleFactory $titleFactory
|
||||
* @param MessageLocalizer $msgLocalizer
|
||||
*/
|
||||
public function __construct(
|
||||
ServiceOptions $options,
|
||||
Language $contentLang,
|
||||
LoggerInterface $logger,
|
||||
TitleFactory $titleFactory,
|
||||
MessageLocalizer $msgLocalizer
|
||||
) {
|
||||
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
||||
$this->options = $options;
|
||||
$this->contentLang = $contentLang;
|
||||
$this->logger = $logger;
|
||||
$this->titleFactory = $titleFactory;
|
||||
$this->msgLocalizer = $msgLocalizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 )
|
||||
|| strpos( $name, '/' ) !== false
|
||||
|| strlen( $name ) > $this->options->get( '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.
|
||||
$title = $this->titleFactory->newFromText( $name );
|
||||
if ( $title === null
|
||||
|| $title->getNamespace()
|
||||
|| strcmp( $name, $title->getPrefixedText() )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check an additional blacklist of troublemaker characters.
|
||||
// Should these be merged into the title char list?
|
||||
$unicodeBlacklist = '/[' .
|
||||
'\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( $unicodeBlacklist, $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( 'ReservedUsernames' );
|
||||
Hooks::run( 'UserGetReservedNames', [ &$reservedUsernames ] );
|
||||
$this->reservedUsernames = $reservedUsernames;
|
||||
}
|
||||
|
||||
// Certain names may be reserved for batch processes.
|
||||
foreach ( $this->reservedUsernames as $reserved ) {
|
||||
if ( substr( $reserved, 0, 4 ) === 'msg:' ) {
|
||||
$reserved = $this->msgLocalizer
|
||||
->msg( substr( $reserved, 4 ) )
|
||||
->inContentLanguage()
|
||||
->plain();
|
||||
}
|
||||
if ( $reserved === $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 blacklisting may be added here rather than in
|
||||
* isValidUserName() 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( '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;
|
||||
}
|
||||
|
||||
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 bool|string
|
||||
*/
|
||||
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 return
|
||||
if ( $validate === self::RIGOR_NONE ) {
|
||||
$name = strtr( $name, '_', ' ' );
|
||||
return $name;
|
||||
}
|
||||
|
||||
// Clean up name according to title rules,
|
||||
// but only when validation is requested (T14654)
|
||||
$title = $this->titleFactory->newFromText( $name, NS_USER );
|
||||
|
||||
// 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.
|
||||
*
|
||||
* Unlike User::isIP, 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 );
|
||||
}
|
||||
|
||||
}
|
||||
418
tests/phpunit/includes/user/UserNameUtilsTest.php
Normal file
418
tests/phpunit/includes/user/UserNameUtilsTest.php
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\User\UserNameUtils;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class UserNameUtilsTest extends MediaWikiTestCase {
|
||||
|
||||
private function getUCFirstLanguageMock() {
|
||||
// Used by a number of tests
|
||||
$contentLang = $this->getMockBuilder( Language::class )
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$contentLang->method( 'ucfirst' )
|
||||
->willReturnCallback( function ( $str ) {
|
||||
return ucfirst( $str );
|
||||
} );
|
||||
return $contentLang;
|
||||
}
|
||||
|
||||
private function getUtils(
|
||||
array $options = [],
|
||||
Language $contentLang = null,
|
||||
LoggerInterface $logger = null,
|
||||
MessageLocalizer $msgLocalizer = null
|
||||
) {
|
||||
$baseOptions = [
|
||||
'MaxNameChars' => 255,
|
||||
'ReservedUsernames' => [
|
||||
'MediaWiki default'
|
||||
],
|
||||
'InvalidUsernameCharacters' => '@:'
|
||||
];
|
||||
$config = $options + $baseOptions;
|
||||
$serviceOptions = new ServiceOptions( UserNameUtils::CONSTRUCTOR_OPTIONS, $config );
|
||||
|
||||
if ( $contentLang === null ) {
|
||||
$contentLang = $this->createMock( Language::class );
|
||||
}
|
||||
|
||||
if ( $logger === null ) {
|
||||
$logger = new NullLogger();
|
||||
}
|
||||
|
||||
// It is almost impossible to mock the TitleFactory, so relying on the real
|
||||
// one. Once its possible to mock, this should be converted to a unit test.
|
||||
$titleFactory = new TitleFactory();
|
||||
|
||||
if ( $msgLocalizer === null ) {
|
||||
$msgLocalizer = $this->createMock( MessageLocalizer::class );
|
||||
}
|
||||
|
||||
$utils = new UserNameUtils(
|
||||
$serviceOptions,
|
||||
$contentLang,
|
||||
$logger,
|
||||
$titleFactory,
|
||||
$msgLocalizer
|
||||
);
|
||||
return $utils;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIsValid
|
||||
* @covers MediaWiki\User\UserNameUtils::isValid
|
||||
*/
|
||||
public function testIsValid( string $name, bool $result ) {
|
||||
$utils = $this->getUtils(
|
||||
[],
|
||||
$this->getUCFirstLanguageMock()
|
||||
);
|
||||
$this->assertSame(
|
||||
$result,
|
||||
$utils->isValid( $name )
|
||||
);
|
||||
}
|
||||
|
||||
public function provideIsValid() {
|
||||
return [
|
||||
'Empty string' => [ '', false ],
|
||||
'Blank space' => [ ' ', false ],
|
||||
'Starts with small letter' => [ 'abcd', false ],
|
||||
'Contains slash' => [ 'Ab/cd', false ],
|
||||
'Whitespace' => [ 'Ab cd', true ],
|
||||
'IP' => [ '192.168.1.1', false ],
|
||||
'IP range' => [ '116.17.184.5/32', false ],
|
||||
'IPv6 range' => [ '::e:f:2001/96', false ],
|
||||
'Reserved Namespace' => [ 'User:Abcd', false ],
|
||||
'Starts with Numbers' => [ '12abcd232', true ],
|
||||
'Start with ? mark' => [ '?abcd', true ],
|
||||
'Start with #' => [ '#abcd', false ],
|
||||
' Mixed scripts' => [ 'Abcdകഖഗഘ', true ],
|
||||
'ZWNJ- Format control character' => [ 'ജോസ്തോമസ്', false ],
|
||||
' Ideographic space' => [ 'Ab cd', false ],
|
||||
'Looks too much like an IPv4 address (1)' => [ '300.300.300.300', false ],
|
||||
'Looks too much like an IPv4 address (2)' => [ '302.113.311.900', false ],
|
||||
'Reserved for usage by UseMod for cloaked logged-out users' => [ '203.0.113.xxx', false ],
|
||||
'Blacklisted characters' => [ "\u{E000}", false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIsUsable
|
||||
* @covers MediaWiki\User\UserNameUtils::isUsable
|
||||
*/
|
||||
public function testIsUsable( string $name, bool $result ) {
|
||||
$msg = $this->getMockBuilder( Message::class )
|
||||
->setMethods( [ 'inContentLanguage', 'plain' ] )
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$msg->method( 'inContentLanguage' )
|
||||
->will( $this->returnSelf() );
|
||||
$msg->method( 'plain' )
|
||||
->willReturn( 'reserved-user' );
|
||||
|
||||
$msgLocalizer = $this->getMockBuilder( MessageLocalizer::class )
|
||||
->setMethods( [ 'msg' ] )
|
||||
->getMock();
|
||||
$msgLocalizer->method( 'msg' )
|
||||
->with( $this->equalTo( 'reserved-user' ) )
|
||||
->willReturn( $msg );
|
||||
|
||||
$utils = $this->getUtils(
|
||||
[
|
||||
'ReservedUsernames' => [
|
||||
'MediaWiki default',
|
||||
'msg:reserved-user'
|
||||
],
|
||||
],
|
||||
$this->getUCFirstLanguageMock(),
|
||||
null,
|
||||
$msgLocalizer
|
||||
);
|
||||
$this->assertSame(
|
||||
$result,
|
||||
$utils->isUsable( $name )
|
||||
);
|
||||
}
|
||||
|
||||
public function provideIsUsable() {
|
||||
return [
|
||||
'Only valid user names are creatable' => [ '', false ],
|
||||
'Reserved names cannot be used' => [ 'MediaWiki default', false ],
|
||||
'Names can also be reserved via msg: ' => [ 'reserved-user', false ],
|
||||
'User names with no issues can be used' => [ 'FooBar', true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers MediaWiki\User\UserNameUtils::isCreatable
|
||||
*/
|
||||
public function testIsCreatable() {
|
||||
$logger = new TestLogger( true, function ( $message ) {
|
||||
$message = str_replace(
|
||||
'MediaWiki\\User\\UserNameUtils::isCreatable: ',
|
||||
'',
|
||||
$message
|
||||
);
|
||||
return $message;
|
||||
} );
|
||||
$utils = $this->getUtils(
|
||||
[],
|
||||
$this->getUCFirstLanguageMock(),
|
||||
$logger
|
||||
);
|
||||
|
||||
$longUserName = str_repeat( 'q', 1000 );
|
||||
$this->assertFalse(
|
||||
$utils->isCreatable( $longUserName ),
|
||||
'longUserName is too long'
|
||||
);
|
||||
$this->assertSame( [
|
||||
[ LogLevel::DEBUG, "'$longUserName' uncreatable due to length" ],
|
||||
], $logger->getBuffer() );
|
||||
$logger->clearBuffer();
|
||||
|
||||
$atUserName = 'Foo@Bar';
|
||||
$this->assertFalse(
|
||||
$utils->isCreatable( $atUserName ),
|
||||
'User name contains invalid character'
|
||||
);
|
||||
$this->assertSame( [
|
||||
[ LogLevel::DEBUG, "'$atUserName' uncreatable due to wgInvalidUsernameCharacters" ],
|
||||
], $logger->getBuffer() );
|
||||
$logger->clearBuffer();
|
||||
|
||||
$this->assertTrue(
|
||||
$utils->isCreatable( 'FooBar' ),
|
||||
'User names with no issues can be created'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetCanonical
|
||||
* @covers MediaWiki\User\UserNameUtils::getCanonical
|
||||
*/
|
||||
public function testGetCanonical( string $name, array $expectedArray ) {
|
||||
$utils = $this->getUtils(
|
||||
[],
|
||||
$this->getUCFirstLanguageMock()
|
||||
);
|
||||
foreach ( $expectedArray as $validate => $expected ) {
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
$utils->getCanonical( $name, $validate ),
|
||||
"Validating '$name' with level '$validate' should be '$expected'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideGetCanonical() {
|
||||
return [
|
||||
'Normal name' => [
|
||||
'Normal name',
|
||||
[
|
||||
UserNameUtils::RIGOR_CREATABLE => 'Normal name',
|
||||
UserNameUtils::RIGOR_USABLE => 'Normal name',
|
||||
UserNameUtils::RIGOR_VALID => 'Normal name',
|
||||
UserNameUtils::RIGOR_NONE => 'Normal name'
|
||||
]
|
||||
],
|
||||
'Leading space' => [
|
||||
' Leading space',
|
||||
[ UserNameUtils::RIGOR_CREATABLE => 'Leading space' ]
|
||||
],
|
||||
'Trailing space' => [
|
||||
'Trailing space ',
|
||||
[ UserNameUtils::RIGOR_CREATABLE => 'Trailing space' ]
|
||||
],
|
||||
'Namespace prefix' => [
|
||||
'Talk:Username',
|
||||
[
|
||||
UserNameUtils::RIGOR_CREATABLE => false,
|
||||
UserNameUtils::RIGOR_USABLE => false,
|
||||
UserNameUtils::RIGOR_VALID => false,
|
||||
UserNameUtils::RIGOR_NONE => 'Talk:Username'
|
||||
]
|
||||
],
|
||||
'With hash' => [
|
||||
'name with # hash',
|
||||
[
|
||||
UserNameUtils::RIGOR_CREATABLE => false,
|
||||
UserNameUtils::RIGOR_USABLE => false
|
||||
]
|
||||
],
|
||||
'Multi spaces' => [
|
||||
'Multi spaces',
|
||||
[
|
||||
UserNameUtils::RIGOR_CREATABLE => 'Multi spaces',
|
||||
UserNameUtils::RIGOR_USABLE => 'Multi spaces'
|
||||
]
|
||||
],
|
||||
'Lowercase' => [
|
||||
'lowercase',
|
||||
[ UserNameUtils::RIGOR_CREATABLE => 'Lowercase' ]
|
||||
],
|
||||
'Invalid character' => [
|
||||
'in[]valid',
|
||||
[
|
||||
UserNameUtils::RIGOR_CREATABLE => false,
|
||||
UserNameUtils::RIGOR_USABLE => false,
|
||||
UserNameUtils::RIGOR_VALID => false,
|
||||
UserNameUtils::RIGOR_NONE => 'In[]valid'
|
||||
]
|
||||
],
|
||||
'With slash' => [
|
||||
'with / slash',
|
||||
[
|
||||
UserNameUtils::RIGOR_CREATABLE => false,
|
||||
UserNameUtils::RIGOR_USABLE => false,
|
||||
UserNameUtils::RIGOR_VALID => false,
|
||||
UserNameUtils::RIGOR_NONE => 'With / slash'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers MediaWiki\User\UserNameUtils::getCanonical
|
||||
*/
|
||||
public function testGetCanonical_interwiki() {
|
||||
// fake interwiki map for the 'Interwiki prefix' testcase
|
||||
$this->setTemporaryHook(
|
||||
'InterwikiLoadPrefix',
|
||||
function ( $prefix, &$iwdata ) {
|
||||
if ( $prefix === 'interwiki' ) {
|
||||
$iwdata = [
|
||||
'iw_url' => 'http://example.com/',
|
||||
'iw_local' => 0,
|
||||
'iw_trans' => 0,
|
||||
];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$utils = $this->getUtils(
|
||||
[],
|
||||
$this->getUCFirstLanguageMock()
|
||||
);
|
||||
|
||||
$name = 'interwiki:Username';
|
||||
$this->assertFalse(
|
||||
$utils->getCanonical(
|
||||
$name,
|
||||
UserNameUtils::RIGOR_CREATABLE
|
||||
),
|
||||
"'$name' is not creatable"
|
||||
);
|
||||
$this->assertFalse(
|
||||
$utils->getCanonical(
|
||||
$name,
|
||||
UserNameUtils::RIGOR_USABLE
|
||||
),
|
||||
"'$name' is not usable"
|
||||
);
|
||||
$this->assertFalse(
|
||||
$utils->getCanonical(
|
||||
$name,
|
||||
UserNameUtils::RIGOR_VALID
|
||||
),
|
||||
"'$name' is not valid"
|
||||
);
|
||||
$this->assertSame(
|
||||
'Interwiki:Username',
|
||||
$utils->getCanonical( $name, UserNameUtils::RIGOR_NONE )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers MediaWiki\User\UserNameUtils::getCanonical
|
||||
*/
|
||||
public function testGetCanonical_bad() {
|
||||
// Only ucfirst is called
|
||||
$utils = $this->getUtils(
|
||||
[],
|
||||
$this->getUCFirstLanguageMock()
|
||||
);
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
$this->expectExceptionMessage( 'Invalid parameter value for validation' );
|
||||
$utils->getCanonical( 'ValidName', 'InvalidValidationValue' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIPs
|
||||
* @covers MediaWiki\User\UserNameUtils::isIP
|
||||
*/
|
||||
public function testIsIP( string $value, bool $result ) {
|
||||
$utils = $this->getUtils();
|
||||
$this->assertSame(
|
||||
$result,
|
||||
$utils->isIP( $value )
|
||||
);
|
||||
}
|
||||
|
||||
public function provideIPs() {
|
||||
return [
|
||||
'Empty string' => [ '', false ],
|
||||
'Blank space' => [ ' ', false ],
|
||||
'IPv4 private 10/8 (1)' => [ '10.0.0.0', true ],
|
||||
'IPv4 private 10/8 (2)' => [ '10.255.255.255', true ],
|
||||
'IPv4 private 192.168/16' => [ '192.168.1.1', true ],
|
||||
'IPv4 example' => [ '203.0.113.0', true ],
|
||||
'IPv6 example' => [ '2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff', true ],
|
||||
// Not valid IPs but classified as such by MediaWiki for negated asserting
|
||||
// of whether this might be the identifier of a logged-out user or whether
|
||||
// to allow usernames like it.
|
||||
'Looks too much like an IPv4 address' => [ '300.300.300.300', true ],
|
||||
'Assigned by UseMod to cloaked logged-out users' => [ '203.0.113.xxx', true ],
|
||||
'Does not accept IPv4 ranges' => [ '74.24.52.13/20', false ],
|
||||
'Does not accept IPv6 ranges' => [ 'fc:100:a:d:1:e:ac:0/24', false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIPRanges
|
||||
* @covers MediaWiki\User\UserNameUtils::isValidIPRange
|
||||
*/
|
||||
public function testIsValidIPRange( $value, $result ) {
|
||||
$utils = $this->getUtils();
|
||||
$this->assertSame(
|
||||
$result,
|
||||
$utils->isValidIPRange( $value )
|
||||
);
|
||||
}
|
||||
|
||||
public function provideIPRanges() {
|
||||
return [
|
||||
[ '116.17.184.5/32', true ],
|
||||
[ '0.17.184.5/30', true ],
|
||||
[ '16.17.184.1/24', true ],
|
||||
[ '30.242.52.14/1', true ],
|
||||
[ '10.232.52.13/8', true ],
|
||||
[ '30.242.52.14/0', true ],
|
||||
[ '::e:f:2001/96', true ],
|
||||
[ '::c:f:2001/128', true ],
|
||||
[ '::10:f:2001/70', true ],
|
||||
[ '::fe:f:2001/1', true ],
|
||||
[ '::6d:f:2001/8', true ],
|
||||
[ '::fe:f:2001/0', true ],
|
||||
[ '116.17.184.5/33', false ],
|
||||
[ '0.17.184.5/130', false ],
|
||||
[ '16.17.184.1/-1', false ],
|
||||
[ '10.232.52.13/*', false ],
|
||||
[ '7.232.52.13/ab', false ],
|
||||
[ '11.232.52.13/', false ],
|
||||
[ '::e:f:2001/129', false ],
|
||||
[ '::c:f:2001/228', false ],
|
||||
[ '::10:f:2001/-1', false ],
|
||||
[ '::6d:f:2001/*', false ],
|
||||
[ '::86:f:2001/ab', false ],
|
||||
[ '::23:f:2001/', false ]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -37,13 +37,6 @@ class UserTest extends MediaWikiTestCase {
|
|||
$this->setUpPermissionGlobals();
|
||||
|
||||
$this->user = $this->getTestUser( 'unittesters' )->getUser();
|
||||
|
||||
TestingAccessWrapper::newFromClass( User::class )->reservedUsernames = false;
|
||||
}
|
||||
|
||||
protected function tearDown() : void {
|
||||
parent::tearDown();
|
||||
TestingAccessWrapper::newFromClass( User::class )->reservedUsernames = false;
|
||||
}
|
||||
|
||||
private function setUpPermissionGlobals() {
|
||||
|
|
@ -685,7 +678,7 @@ class UserTest extends MediaWikiTestCase {
|
|||
public function testGetCanonicalName_bad() {
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
$this->expectExceptionMessage(
|
||||
'Invalid parameter value for $validate in User::getCanonicalName'
|
||||
'Invalid parameter value for validation'
|
||||
);
|
||||
User::getCanonicalName( 'ValidName', 'InvalidValidationValue' );
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue