MediaWiki master
UserDef.php
Go to the documentation of this file.
1<?php
2
4
13use Wikimedia\IPUtils;
18
27class UserDef extends TypeDef {
28
46 public const PARAM_ALLOWED_USER_TYPES = 'param-allowed-user-types';
47
57 public const PARAM_RETURN_OBJECT = 'param-return-object';
58
60 private $userIdentityLookup;
61
63 private $titleParser;
64
66 private $userNameUtils;
67
74 public function __construct(
76 UserIdentityLookup $userIdentityLookup,
77 TitleParser $titleParser,
78 UserNameUtils $userNameUtils
79 ) {
80 parent::__construct( $callbacks );
81 $this->userIdentityLookup = $userIdentityLookup;
82 $this->titleParser = $titleParser;
83 $this->userNameUtils = $userNameUtils;
84 }
85
86 public function validate( $name, $value, array $settings, array $options ) {
87 $this->failIfNotString( $name, $value, $settings, $options );
88
89 [ $type, $user ] = $this->processUser( $value );
90
91 if ( !$user || !in_array( $type, $settings[self::PARAM_ALLOWED_USER_TYPES], true ) ) {
92 // Message used: paramvalidator-baduser
93 $this->failure( 'baduser', $name, $value, $settings, $options );
94 }
95
96 return empty( $settings[self::PARAM_RETURN_OBJECT] ) ? $user->getName() : $user;
97 }
98
99 public function normalizeSettings( array $settings ) {
100 if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
101 $settings[self::PARAM_ALLOWED_USER_TYPES] = array_values( array_intersect(
102 [ 'name', 'ip', 'temp', 'cidr', 'interwiki', 'id' ],
103 $settings[self::PARAM_ALLOWED_USER_TYPES]
104 ) );
105 }
106 if ( empty( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
107 $settings[self::PARAM_ALLOWED_USER_TYPES] = [ 'name', 'ip', 'temp', 'cidr', 'interwiki' ];
108 }
109
110 return parent::normalizeSettings( $settings );
111 }
112
113 public function checkSettings( string $name, $settings, array $options, array $ret ): array {
114 $ret = parent::checkSettings( $name, $settings, $options, $ret );
115
116 $ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
117 self::PARAM_ALLOWED_USER_TYPES, self::PARAM_RETURN_OBJECT,
118 ] );
119
120 if ( !is_bool( $settings[self::PARAM_RETURN_OBJECT] ?? false ) ) {
121 $ret['issues'][self::PARAM_RETURN_OBJECT] = 'PARAM_RETURN_OBJECT must be boolean, got '
122 . gettype( $settings[self::PARAM_RETURN_OBJECT] );
123 }
124
125 $hasId = false;
126 if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
127 if ( !is_array( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
128 $ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES must be an array, '
129 . 'got ' . gettype( $settings[self::PARAM_ALLOWED_USER_TYPES] );
130 } elseif ( $settings[self::PARAM_ALLOWED_USER_TYPES] === [] ) {
131 $ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES cannot be empty';
132 } else {
133 $bad = array_diff(
134 $settings[self::PARAM_ALLOWED_USER_TYPES],
135 [ 'name', 'ip', 'temp', 'cidr', 'interwiki', 'id' ]
136 );
137 if ( $bad ) {
138 $ret['issues'][self::PARAM_ALLOWED_USER_TYPES] =
139 'PARAM_ALLOWED_USER_TYPES contains invalid values: ' . implode( ', ', $bad );
140 }
141
142 $hasId = in_array( 'id', $settings[self::PARAM_ALLOWED_USER_TYPES], true );
143 }
144 }
145
146 if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
147 ( $hasId || !empty( $settings[self::PARAM_RETURN_OBJECT] ) ) &&
148 (
149 ( $settings[ParamValidator::PARAM_ISMULTI_LIMIT1] ?? 100 ) > 10 ||
150 ( $settings[ParamValidator::PARAM_ISMULTI_LIMIT2] ?? 100 ) > 10
151 )
152 ) {
153 $ret['issues'][] = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
154 . 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
155 . ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
156 . 'once we have a real use case to look at.)';
157 }
158
159 return $ret;
160 }
161
168 private function processUser( string $value ): array {
169 // A user ID?
170 if ( preg_match( '/^#(\d+)$/D', $value, $m ) ) {
171 // This used to use the IP address of the current request if the
172 // id was 0, to match the behavior of User objects, but was switched
173 // to "Unknown user" because the former behavior is likely unexpected.
174 // If the id corresponds to a user in the database, use that user, otherwise
175 // return a UserIdentityValue with id 0 (regardless of the input id) and
176 // the name "Unknown user"
177 $userId = (int)$m[1];
178 if ( $userId !== 0 ) {
179 // Check the database.
180 $userIdentity = $this->userIdentityLookup->getUserIdentityByUserId( $userId );
181 if ( $userIdentity ) {
182 return [ 'id', $userIdentity ];
183 }
184 }
185 // Fall back to "Unknown user"
186 return [
187 'id',
188 new UserIdentityValue( 0, "Unknown user" )
189 ];
190 }
191
192 // An interwiki username?
193 if ( ExternalUserNames::isExternal( $value ) ) {
194 $name = $this->userNameUtils->getCanonical( $value, UserRigorOptions::RIGOR_NONE );
195 // UserIdentityValue has the username which includes the > separating the external
196 // wiki database and the actual name, but is created for the *local* wiki, like
197 // for User objects (local is the default, but we specify it anyway to show
198 // that its intentional even though the username is for a different wiki)
199 // NOTE: We deliberately use the raw $value instead of the canonical $name
200 // to avoid converting the first character of the interwiki prefix to uppercase
201 $user = $name !== false ? new UserIdentityValue( 0, $value, UserIdentityValue::LOCAL ) : null;
202 return [ 'interwiki', $user ];
203 }
204
205 // A valid user name?
206 // Match behavior of UserFactory::newFromName with RIGOR_VALID and User::getId()
207 // we know that if there is a canonical form from UserNameUtils then this can't
208 // look like an IP, and since we checked for external user names above it isn't
209 // that either, so if this is a valid user name then we check the database for
210 // the id, and if there is no user with this name the id is 0
211 $canonicalName = $this->userNameUtils->getCanonical( $value, UserRigorOptions::RIGOR_VALID );
212 if ( $canonicalName !== false ) {
213 // Determine if the username matches the temporary account format.
214 $userType = $this->userNameUtils->isTemp( $value ) ? 'temp' : 'name';
215
216 $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $canonicalName );
217 if ( $userIdentity ) {
218 return [ $userType, $userIdentity ];
219 }
220 // Fall back to id 0, which can occur when the account does not exist.
221 return [
222 $userType,
223 new UserIdentityValue( 0, $canonicalName )
224 ];
225 }
226
227 // (T232672) Reproduce the normalization applied in UserNameUtils::getCanonical() when
228 // performing the checks below.
229 if ( strpos( $value, '#' ) !== false ) {
230 return [ '', null ];
231 }
232
233 try {
234 $t = $this->titleParser->parseTitle( $value );
235 } catch ( MalformedTitleException $_ ) {
236 $t = null;
237 }
238 if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) { // likely
239 try {
240 $t = $this->titleParser->parseTitle( "User:$value" );
241 } catch ( MalformedTitleException $_ ) {
242 $t = null;
243 }
244 }
245 if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
246 // If it wasn't a valid User-namespace title, fail.
247 return [ '', null ];
248 }
249 $value = $t->getText();
250
251 // An IP?
252 $b = IPUtils::RE_IP_BYTE;
253 if ( IPUtils::isValid( $value ) ||
254 // See comment for UserNameUtils::isIP. We don't just call that function
255 // here because it also returns true for things like
256 // 300.300.300.300 that are neither valid usernames nor valid IP
257 // addresses.
258 preg_match( "/^$b\.$b\.$b\.xxx$/D", $value )
259 ) {
260 $name = IPUtils::sanitizeIP( $value );
261 // We don't really need to use UserNameUtils::getCanonical() because for anonymous
262 // users the only validation is that there is no `#` (which is already the case if its
263 // a valid IP or matches the regex) and the only normalization is making the first
264 // character uppercase (doesn't matter for numbers) and replacing underscores with
265 // spaces (doesn't apply to IPs). But, better safe than sorry?
266 $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
267 return [ 'ip', UserIdentityValue::newAnonymous( $name ) ];
268 }
269
270 // A range?
271 if ( IPUtils::isValidRange( $value ) ) {
272 $name = IPUtils::sanitizeIP( $value );
273 // Per above, the UserNameUtils call isn't strictly needed, but doesn't hurt
274 $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
275 return [ 'cidr', UserIdentityValue::newAnonymous( $name ) ];
276 }
277
278 // Fail.
279 return [ '', null ];
280 }
281
282 public function getParamInfo( $name, array $settings, array $options ) {
283 $info = parent::getParamInfo( $name, $settings, $options );
284
285 $info['subtypes'] = $settings[self::PARAM_ALLOWED_USER_TYPES];
286
287 return $info;
288 }
289
290 public function getHelpInfo( $name, array $settings, array $options ) {
291 $info = parent::getParamInfo( $name, $settings, $options );
292
293 $isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] );
294
295 $subtypes = [];
296 foreach ( $settings[self::PARAM_ALLOWED_USER_TYPES] as $st ) {
297 // Messages: paramvalidator-help-type-user-subtype-name,
298 // paramvalidator-help-type-user-subtype-ip, paramvalidator-help-type-user-subtype-cidr,
299 // paramvalidator-help-type-user-subtype-interwiki, paramvalidator-help-type-user-subtype-id,
300 // paramvalidator-help-type-user-subtype-temp
301 $subtypes[] = MessageValue::new( "paramvalidator-help-type-user-subtype-$st" );
302 }
303 $info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-user' )
304 ->params( $isMulti ? 2 : 1 )
305 ->textListParams( $subtypes )
306 ->numParams( count( $subtypes ) );
307
308 return $info;
309 }
310
311}
const NS_USER
Definition Defines.php:67
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Type definition for user types.
Definition UserDef.php:27
const PARAM_RETURN_OBJECT
(bool) Whether to return a UserIdentity object.
Definition UserDef.php:57
checkSettings(string $name, $settings, array $options, array $ret)
Validate a parameter settings array.
Definition UserDef.php:113
validate( $name, $value, array $settings, array $options)
Validate the value.
Definition UserDef.php:86
getHelpInfo( $name, array $settings, array $options)
Describe parameter settings in human-readable format.
Definition UserDef.php:290
__construct(Callbacks $callbacks, UserIdentityLookup $userIdentityLookup, TitleParser $titleParser, UserNameUtils $userNameUtils)
Definition UserDef.php:74
normalizeSettings(array $settings)
Normalize a settings array.
Definition UserDef.php:99
const PARAM_ALLOWED_USER_TYPES
(string[]) Allowed types of user.
Definition UserDef.php:46
getParamInfo( $name, array $settings, array $options)
Describe parameter settings in a machine-readable format.
Definition UserDef.php:282
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Class to parse and build external user names.
Value object representing a user's identity.
UserNameUtils service.
Value object representing a message for i18n.
Service for formatting and validating API parameters.
Base definition for ParamValidator types.
Definition TypeDef.php:19
failIfNotString(string $name, $value, array $settings, array $options)
Fails if $value is not a string.
Definition TypeDef.php:68
failure( $failure, $name, $value, array $settings, array $options, $fatal=true)
Record a failure message.
Definition TypeDef.php:121
A title parser service for MediaWiki.
Service for looking up UserIdentity.
Interface for objects representing user identity.
Shared interface for rigor levels when dealing with User methods.
Interface defining callbacks needed by ParamValidator.
Definition Callbacks.php:21