MediaWiki  master
AuthenticationRequest.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
27 use Message;
28 use UnexpectedValueException;
29 
40 abstract class AuthenticationRequest {
41 
43  public const OPTIONAL = 0;
44 
49  public const REQUIRED = 1;
50 
55  public const PRIMARY_REQUIRED = 2;
56 
61  public $action = null;
62 
67 
69  public $returnToUrl = null;
70 
74  public $username = null;
75 
92  public function getUniqueId() {
93  return get_called_class();
94  }
95 
131  abstract public function getFieldInfo();
132 
144  public function getMetadata() {
145  return [];
146  }
147 
161  public function loadFromSubmission( array $data ) {
162  $fields = array_filter( $this->getFieldInfo(), static function ( $info ) {
163  return $info['type'] !== 'null';
164  } );
165  if ( !$fields ) {
166  return false;
167  }
168 
169  foreach ( $fields as $field => $info ) {
170  // Checkboxes and buttons are special. Depending on the method used
171  // to populate $data, they might be unset meaning false or they
172  // might be boolean. Further, image buttons might submit the
173  // coordinates of the click rather than the expected value.
174  if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
175  $this->$field = isset( $data[$field] ) && $data[$field] !== false
176  || isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
177  if ( !$this->$field && empty( $info['optional'] ) ) {
178  return false;
179  }
180  continue;
181  }
182 
183  // Multiselect are too, slightly
184  if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
185  $data[$field] = [];
186  }
187 
188  if ( !isset( $data[$field] ) ) {
189  return false;
190  }
191  if ( $data[$field] === '' || $data[$field] === [] ) {
192  if ( empty( $info['optional'] ) ) {
193  return false;
194  }
195  } else {
196  switch ( $info['type'] ) {
197  case 'select':
198  if ( !isset( $info['options'][$data[$field]] ) ) {
199  return false;
200  }
201  break;
202 
203  case 'multiselect':
204  $data[$field] = (array)$data[$field];
205  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset required for multiselect
206  $allowed = array_keys( $info['options'] );
207  if ( array_diff( $data[$field], $allowed ) !== [] ) {
208  return false;
209  }
210  break;
211  }
212  }
213 
214  $this->$field = $data[$field];
215  }
216 
217  return true;
218  }
219 
238  public function describeCredentials() {
239  return [
240  'provider' => new RawMessage( '$1', [ get_called_class() ] ),
241  'account' => new RawMessage( '$1', [ $this->getUniqueId() ] ),
242  ];
243  }
244 
252  public static function loadRequestsFromSubmission( array $reqs, array $data ) {
253  $result = [];
254  foreach ( $reqs as $req ) {
255  if ( $req->loadFromSubmission( $data ) ) {
256  $result[] = $req;
257  }
258  }
259  return $result;
260  }
261 
275  public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
276  $requests = array_filter( $reqs, static function ( $req ) use ( $class, $allowSubclasses ) {
277  if ( $allowSubclasses ) {
278  return is_a( $req, $class, false );
279  } else {
280  return get_class( $req ) === $class;
281  }
282  } );
283  // @phan-suppress-next-line PhanTypeMismatchReturn False positive
284  return count( $requests ) === 1 ? reset( $requests ) : null;
285  }
286 
296  public static function getUsernameFromRequests( array $reqs ) {
297  $username = null;
298  $otherClass = null;
299  foreach ( $reqs as $req ) {
300  $info = $req->getFieldInfo();
301  if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
302  if ( $username === null ) {
303  $username = $req->username;
304  $otherClass = get_class( $req );
305  } elseif ( $username !== $req->username ) {
306  $requestClass = get_class( $req );
307  throw new UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
308  // @phan-suppress-next-line PhanTypeSuspiciousStringExpression $otherClass always set
309  . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
310  }
311  }
312  }
313  return $username;
314  }
315 
322  public static function mergeFieldInfo( array $reqs ) {
323  $merged = [];
324 
325  // fields that are required by some primary providers but not others are not actually required
326  $sharedRequiredPrimaryFields = null;
327  foreach ( $reqs as $req ) {
328  if ( $req->required !== self::PRIMARY_REQUIRED ) {
329  continue;
330  }
331  $required = [];
332  foreach ( $req->getFieldInfo() as $fieldName => $options ) {
333  if ( empty( $options['optional'] ) ) {
334  $required[] = $fieldName;
335  }
336  }
337  if ( $sharedRequiredPrimaryFields === null ) {
338  $sharedRequiredPrimaryFields = $required;
339  } else {
340  $sharedRequiredPrimaryFields = array_intersect( $sharedRequiredPrimaryFields, $required );
341  }
342  }
343 
344  foreach ( $reqs as $req ) {
345  $info = $req->getFieldInfo();
346  if ( !$info ) {
347  continue;
348  }
349 
350  foreach ( $info as $name => $options ) {
351  if (
352  // If the request isn't required, its fields aren't required either.
353  $req->required === self::OPTIONAL
354  // If there is a primary not requiring this field, no matter how many others do,
355  // authentication can proceed without it.
356  || $req->required === self::PRIMARY_REQUIRED
357  // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal False positive
358  && !in_array( $name, $sharedRequiredPrimaryFields, true )
359  ) {
360  $options['optional'] = true;
361  } else {
362  $options['optional'] = !empty( $options['optional'] );
363  }
364 
365  $options['sensitive'] = !empty( $options['sensitive'] );
366  $type = $options['type'];
367 
368  if ( !array_key_exists( $name, $merged ) ) {
369  $merged[$name] = $options;
370  } elseif ( $merged[$name]['type'] !== $type ) {
371  throw new UnexpectedValueException( "Field type conflict for \"$name\", " .
372  "\"{$merged[$name]['type']}\" vs \"$type\""
373  );
374  } else {
375  if ( isset( $options['options'] ) ) {
376  if ( isset( $merged[$name]['options'] ) ) {
377  $merged[$name]['options'] += $options['options'];
378  } else {
379  // @codeCoverageIgnoreStart
380  $merged[$name]['options'] = $options['options'];
381  // @codeCoverageIgnoreEnd
382  }
383  }
384 
385  $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
386  $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive'];
387 
388  // No way to merge 'value', 'image', 'help', or 'label', so just use
389  // the value from the first request.
390  }
391  }
392  }
393 
394  return $merged;
395  }
396 
402  public static function __set_state( $data ) {
403  // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
404  $ret = new static();
405  foreach ( $data as $k => $v ) {
406  $ret->$k = $v;
407  }
408  return $ret;
409  }
410 }
This is a value object for authentication requests.
getFieldInfo()
Fetch input field info.
string null $returnToUrl
Return-to URL, in case of redirect.
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
string null $action
The AuthManager::ACTION_* constant this request was created to be used for.
static __set_state( $data)
Implementing this mainly for use from the unit tests.
getUniqueId()
Supply a unique key for deduplication.
int $required
For login, continue, and link actions, one of self::OPTIONAL, self::REQUIRED, or self::PRIMARY_REQUIR...
static mergeFieldInfo(array $reqs)
Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
static loadRequestsFromSubmission(array $reqs, array $data)
Update a set of requests with form submit data, discarding ones that fail.
describeCredentials()
Describe the credentials represented by this request.
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
getMetadata()
Returns metadata about this request.
const REQUIRED
Indicates that the request is required for authentication to proceed.
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
loadFromSubmission(array $data)
Initialize form submitted form data.
Variant of the Message class.
Definition: RawMessage.php:40
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:144