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
|
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use MediaWiki\Title\MalformedTitleException;
use MediaWiki\Title\TitleParser;
use MediaWiki\User\ExternalUserNames;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserIdentityValue;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserRigorOptions;
use Wikimedia\IPUtils;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
/**
* Type definition for user types
*
* Failure codes:
* - 'baduser': The value was not a valid MediaWiki user. No data.
*
* @since 1.35
*/
class UserDef extends TypeDef {
/**
* (string[]) Allowed types of user.
*
* One or more of the following values:
* - 'name': User names are allowed.
* - 'ip': IP ("anon") usernames are allowed.
* - 'temp': Temporary users are allowed.
* - 'cidr': IP ranges are allowed.
* - 'interwiki': Interwiki usernames are allowed.
* - 'id': Allow specifying user IDs, formatted like "#123".
*
* Default is `[ 'name', 'ip', 'temp', 'cidr', 'interwiki' ]`.
*
* Avoid combining 'id' with PARAM_ISMULTI, as it may result in excessive
* DB lookups. If you do combine them, consider setting low values for
* PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
*/
public const PARAM_ALLOWED_USER_TYPES = 'param-allowed-user-types';
/**
* (bool) Whether to return a UserIdentity object.
*
* If false, the validated user name is returned as a string. Default is false.
*
* Avoid setting true with PARAM_ISMULTI, as it may result in excessive DB
* lookups. If you do combine them, consider setting low values for
* PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
*/
public const PARAM_RETURN_OBJECT = 'param-return-object';
/** @var UserIdentityLookup */
private $userIdentityLookup;
/** @var TitleParser */
private $titleParser;
/** @var UserNameUtils */
private $userNameUtils;
/**
* @param Callbacks $callbacks
* @param UserIdentityLookup $userIdentityLookup
* @param TitleParser $titleParser
* @param UserNameUtils $userNameUtils
*/
public function __construct(
Callbacks $callbacks,
UserIdentityLookup $userIdentityLookup,
TitleParser $titleParser,
UserNameUtils $userNameUtils
) {
parent::__construct( $callbacks );
$this->userIdentityLookup = $userIdentityLookup;
$this->titleParser = $titleParser;
$this->userNameUtils = $userNameUtils;
}
public function validate( $name, $value, array $settings, array $options ) {
$this->failIfNotString( $name, $value, $settings, $options );
[ $type, $user ] = $this->processUser( $value );
if ( !$user || !in_array( $type, $settings[self::PARAM_ALLOWED_USER_TYPES], true ) ) {
// Message used: paramvalidator-baduser
$this->failure( 'baduser', $name, $value, $settings, $options );
}
return empty( $settings[self::PARAM_RETURN_OBJECT] ) ? $user->getName() : $user;
}
public function normalizeSettings( array $settings ) {
if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
$settings[self::PARAM_ALLOWED_USER_TYPES] = array_values( array_intersect(
[ 'name', 'ip', 'temp', 'cidr', 'interwiki', 'id' ],
$settings[self::PARAM_ALLOWED_USER_TYPES]
) );
}
if ( empty( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
$settings[self::PARAM_ALLOWED_USER_TYPES] = [ 'name', 'ip', 'temp', 'cidr', 'interwiki' ];
}
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ): array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_ALLOWED_USER_TYPES, self::PARAM_RETURN_OBJECT,
] );
if ( !is_bool( $settings[self::PARAM_RETURN_OBJECT] ?? false ) ) {
$ret['issues'][self::PARAM_RETURN_OBJECT] = 'PARAM_RETURN_OBJECT must be boolean, got '
. gettype( $settings[self::PARAM_RETURN_OBJECT] );
}
$hasId = false;
if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
if ( !is_array( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES must be an array, '
. 'got ' . gettype( $settings[self::PARAM_ALLOWED_USER_TYPES] );
} elseif ( $settings[self::PARAM_ALLOWED_USER_TYPES] === [] ) {
$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES cannot be empty';
} else {
$bad = array_diff(
$settings[self::PARAM_ALLOWED_USER_TYPES],
[ 'name', 'ip', 'temp', 'cidr', 'interwiki', 'id' ]
);
if ( $bad ) {
$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] =
'PARAM_ALLOWED_USER_TYPES contains invalid values: ' . implode( ', ', $bad );
}
$hasId = in_array( 'id', $settings[self::PARAM_ALLOWED_USER_TYPES], true );
}
}
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
( $hasId || !empty( $settings[self::PARAM_RETURN_OBJECT] ) ) &&
(
( $settings[ParamValidator::PARAM_ISMULTI_LIMIT1] ?? 100 ) > 10 ||
( $settings[ParamValidator::PARAM_ISMULTI_LIMIT2] ?? 100 ) > 10
)
) {
$ret['issues'][] = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
. 'once we have a real use case to look at.)';
}
return $ret;
}
/**
* Process $value to a UserIdentity, if possible
* @param string $value
* @return array [ string $type, UserIdentity|null $user ]
* @phan-return array{0:string,1:UserIdentity|null}
*/
private function processUser( string $value ): array {
// A user ID?
if ( preg_match( '/^#(\d+)$/D', $value, $m ) ) {
// This used to use the IP address of the current request if the
// id was 0, to match the behavior of User objects, but was switched
// to "Unknown user" because the former behavior is likely unexpected.
// If the id corresponds to a user in the database, use that user, otherwise
// return a UserIdentityValue with id 0 (regardless of the input id) and
// the name "Unknown user"
$userId = (int)$m[1];
if ( $userId !== 0 ) {
// Check the database.
$userIdentity = $this->userIdentityLookup->getUserIdentityByUserId( $userId );
if ( $userIdentity ) {
return [ 'id', $userIdentity ];
}
}
// Fall back to "Unknown user"
return [
'id',
new UserIdentityValue( 0, "Unknown user" )
];
}
// An interwiki username?
if ( ExternalUserNames::isExternal( $value ) ) {
$name = $this->userNameUtils->getCanonical( $value, UserRigorOptions::RIGOR_NONE );
// UserIdentityValue has the username which includes the > separating the external
// wiki database and the actual name, but is created for the *local* wiki, like
// for User objects (local is the default, but we specify it anyway to show
// that its intentional even though the username is for a different wiki)
// NOTE: We deliberately use the raw $value instead of the canonical $name
// to avoid converting the first character of the interwiki prefix to uppercase
$user = $name !== false ? new UserIdentityValue( 0, $value, UserIdentityValue::LOCAL ) : null;
return [ 'interwiki', $user ];
}
// A temp user?
if ( $this->userNameUtils->isTemp( $value ) ) {
$userIdentity = $this->userIdentityLookup->getUserIdentityByName( $value );
return [ 'temp', $userIdentity ];
}
// A valid user name?
// Match behavior of UserFactory::newFromName with RIGOR_VALID and User::getId()
// we know that if there is a canonical form from UserNameUtils then this can't
// look like an IP, and since we checked for external user names above it isn't
// that either, so if this is a valid user name then we check the database for
// the id, and if there is no user with this name the id is 0
$canonicalName = $this->userNameUtils->getCanonical( $value, UserRigorOptions::RIGOR_VALID );
if ( $canonicalName !== false ) {
$userIdentity = $this->userIdentityLookup->getUserIdentityByName( $canonicalName );
if ( $userIdentity ) {
return [ 'name', $userIdentity ];
}
// Fall back to id 0
return [
'name',
new UserIdentityValue( 0, $canonicalName )
];
}
// (T232672) Reproduce the normalization applied in UserNameUtils::getCanonical() when
// performing the checks below.
if ( strpos( $value, '#' ) !== false ) {
return [ '', null ];
}
try {
$t = $this->titleParser->parseTitle( $value );
} catch ( MalformedTitleException $_ ) {
$t = null;
}
if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) { // likely
try {
$t = $this->titleParser->parseTitle( "User:$value" );
} catch ( MalformedTitleException $_ ) {
$t = null;
}
}
if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
// If it wasn't a valid User-namespace title, fail.
return [ '', null ];
}
$value = $t->getText();
// An IP?
$b = IPUtils::RE_IP_BYTE;
if ( IPUtils::isValid( $value ) ||
// See comment for UserNameUtils::isIP. We don't just call that function
// here because it also returns true for things like
// 300.300.300.300 that are neither valid usernames nor valid IP
// addresses.
preg_match( "/^$b\.$b\.$b\.xxx$/D", $value )
) {
$name = IPUtils::sanitizeIP( $value );
// We don't really need to use UserNameUtils::getCanonical() because for anonymous
// users the only validation is that there is no `#` (which is already the case if its
// a valid IP or matches the regex) and the only normalization is making the first
// character uppercase (doesn't matter for numbers) and replacing underscores with
// spaces (doesn't apply to IPs). But, better safe than sorry?
$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
return [ 'ip', UserIdentityValue::newAnonymous( $name ) ];
}
// A range?
if ( IPUtils::isValidRange( $value ) ) {
$name = IPUtils::sanitizeIP( $value );
// Per above, the UserNameUtils call isn't strictly needed, but doesn't hurt
$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
return [ 'cidr', UserIdentityValue::newAnonymous( $name ) ];
}
// Fail.
return [ '', null ];
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$info['subtypes'] = $settings[self::PARAM_ALLOWED_USER_TYPES];
return $info;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] );
$subtypes = [];
foreach ( $settings[self::PARAM_ALLOWED_USER_TYPES] as $st ) {
// Messages: paramvalidator-help-type-user-subtype-name,
// paramvalidator-help-type-user-subtype-ip, paramvalidator-help-type-user-subtype-cidr,
// paramvalidator-help-type-user-subtype-interwiki, paramvalidator-help-type-user-subtype-id,
// paramvalidator-help-type-user-subtype-temp
$subtypes[] = MessageValue::new( "paramvalidator-help-type-user-subtype-$st" );
}
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-user' )
->params( $isMulti ? 2 : 1 )
->textListParams( $subtypes )
->numParams( count( $subtypes ) );
return $info;
}
}
|