Migrate all callers of Hooks::run() to use the new HookContainer/HookRunner system. General principles: * Use DI if it is already used. We're not changing the way state is managed in this patch. * HookContainer is always injected, not HookRunner. HookContainer is a service, it's a more generic interface, it is the only thing that provides isRegistered() which is needed in some cases, and a HookRunner can be efficiently constructed from it (confirmed by benchmark). Because HookContainer is needed for object construction, it is also needed by all factories. * "Ask your friendly local base class". Big hierarchies like SpecialPage and ApiBase have getHookContainer() and getHookRunner() methods in the base class, and classes that extend that base class are not expected to know or care where the base class gets its HookContainer from. * ProtectedHookAccessorTrait provides protected getHookContainer() and getHookRunner() methods, getting them from the global service container. The point of this is to ease migration to DI by ensuring that call sites ask their local friendly base class rather than getting a HookRunner from the service container directly. * Private $this->hookRunner. In some smaller classes where accessor methods did not seem warranted, there is a private HookRunner property which is accessed directly. Very rarely (two cases), there is a protected property, for consistency with code that conventionally assumes protected=private, but in cases where the class might actually be overridden, a protected accessor is preferred over a protected property. * The last resort: Hooks::runner(). Mostly for static, file-scope and global code. In a few cases it was used for objects with broken construction schemes, out of horror or laziness. Constructors with new required arguments: * AuthManager * BadFileLookup * BlockManager * ClassicInterwikiLookup * ContentHandlerFactory * ContentSecurityPolicy * DefaultOptionsManager * DerivedPageDataUpdater * FullSearchResultWidget * HtmlCacheUpdater * LanguageFactory * LanguageNameUtils * LinkRenderer * LinkRendererFactory * LocalisationCache * MagicWordFactory * MessageCache * NamespaceInfo * PageEditStash * PageHandlerFactory * PageUpdater * ParserFactory * PermissionManager * RevisionStore * RevisionStoreFactory * SearchEngineConfig * SearchEngineFactory * SearchFormWidget * SearchNearMatcher * SessionBackend * SpecialPageFactory * UserNameUtils * UserOptionsManager * WatchedItemQueryService * WatchedItemStore Constructors with new optional arguments: * DefaultPreferencesFactory * Language * LinkHolderArray * MovePage * Parser * ParserCache * PasswordReset * Router setHookContainer() now required after construction: * AuthenticationProvider * ResourceLoaderModule * SearchEngine Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
341 lines
9.3 KiB
PHP
341 lines
9.3 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 MediaWiki\Config\ServiceOptions;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\HookRunner;
|
|
use Psr\Log\LoggerInterface;
|
|
use TitleFactory;
|
|
use Wikimedia\IPUtils;
|
|
use Wikimedia\Message\ITextFormatter;
|
|
use Wikimedia\Message\MessageValue;
|
|
|
|
/**
|
|
* 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 ITextFormatter
|
|
*/
|
|
private $textFormatter;
|
|
|
|
/**
|
|
* @var string[]|false Cache for isUsable()
|
|
*/
|
|
private $reservedUsernames = false;
|
|
|
|
/**
|
|
* @var HookRunner
|
|
*/
|
|
private $hookRunner;
|
|
|
|
/**
|
|
* @param ServiceOptions $options
|
|
* @param Language $contentLang
|
|
* @param LoggerInterface $logger
|
|
* @param TitleFactory $titleFactory
|
|
* @param ITextFormatter $textFormatter the text formatter for the current content language
|
|
* @param HookContainer $hookContainer
|
|
*/
|
|
public function __construct(
|
|
ServiceOptions $options,
|
|
Language $contentLang,
|
|
LoggerInterface $logger,
|
|
TitleFactory $titleFactory,
|
|
ITextFormatter $textFormatter,
|
|
HookContainer $hookContainer
|
|
) {
|
|
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
|
$this->options = $options;
|
|
$this->contentLang = $contentLang;
|
|
$this->logger = $logger;
|
|
$this->titleFactory = $titleFactory;
|
|
$this->textFormatter = $textFormatter;
|
|
$this->hookRunner = new HookRunner( $hookContainer );
|
|
}
|
|
|
|
/**
|
|
* 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' );
|
|
$this->hookRunner->onUserGetReservedNames( $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->textFormatter->format(
|
|
MessageValue::new( substr( $reserved, 4 ) )
|
|
);
|
|
}
|
|
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 );
|
|
}
|
|
|
|
}
|