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