MediaWiki  master
AuthenticationRequest.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
26 use Message;
27 
37 abstract class AuthenticationRequest {
38 
40  const OPTIONAL = 0;
41 
46  const REQUIRED = 1;
47 
52  const PRIMARY_REQUIRED = 2;
53 
58  public $action = null;
59 
63  public $required = self::REQUIRED;
64 
66  public $returnToUrl = null;
67 
71  public $username = null;
72 
88  public function getUniqueId() {
89  return get_called_class();
90  }
91 
127  abstract public function getFieldInfo();
128 
139  public function getMetadata() {
140  return [];
141  }
142 
155  public function loadFromSubmission( array $data ) {
156  $fields = array_filter( $this->getFieldInfo(), function ( $info ) {
157  return $info['type'] !== 'null';
158  } );
159  if ( !$fields ) {
160  return false;
161  }
162 
163  foreach ( $fields as $field => $info ) {
164  // Checkboxes and buttons are special. Depending on the method used
165  // to populate $data, they might be unset meaning false or they
166  // might be boolean. Further, image buttons might submit the
167  // coordinates of the click rather than the expected value.
168  if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
169  $this->$field = isset( $data[$field] ) && $data[$field] !== false
170  || isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
171  if ( !$this->$field && empty( $info['optional'] ) ) {
172  return false;
173  }
174  continue;
175  }
176 
177  // Multiselect are too, slightly
178  if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
179  $data[$field] = [];
180  }
181 
182  if ( !isset( $data[$field] ) ) {
183  return false;
184  }
185  if ( $data[$field] === '' || $data[$field] === [] ) {
186  if ( empty( $info['optional'] ) ) {
187  return false;
188  }
189  } else {
190  switch ( $info['type'] ) {
191  case 'select':
192  if ( !isset( $info['options'][$data[$field]] ) ) {
193  return false;
194  }
195  break;
196 
197  case 'multiselect':
198  $data[$field] = (array)$data[$field];
199  $allowed = array_keys( $info['options'] );
200  if ( array_diff( $data[$field], $allowed ) !== [] ) {
201  return false;
202  }
203  break;
204  }
205  }
206 
207  $this->$field = $data[$field];
208  }
209 
210  return true;
211  }
212 
229  public function describeCredentials() {
230  return [
231  'provider' => new \RawMessage( '$1', [ get_called_class() ] ),
232  'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ),
233  ];
234  }
235 
242  public static function loadRequestsFromSubmission( array $reqs, array $data ) {
243  return array_values( array_filter( $reqs, function ( $req ) use ( $data ) {
244  return $req->loadFromSubmission( $data );
245  } ) );
246  }
247 
263  public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
264  $requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) {
265  if ( $allowSubclasses ) {
266  return is_a( $req, $class, false );
267  } else {
268  return get_class( $req ) === $class;
269  }
270  } );
271  return count( $requests ) === 1 ? reset( $requests ) : null;
272  }
273 
283  public static function getUsernameFromRequests( array $reqs ) {
284  $username = null;
285  $otherClass = null;
286  foreach ( $reqs as $req ) {
287  $info = $req->getFieldInfo();
288  if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
289  if ( $username === null ) {
290  $username = $req->username;
291  $otherClass = get_class( $req );
292  } elseif ( $username !== $req->username ) {
293  $requestClass = get_class( $req );
294  throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
295  . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
296  }
297  }
298  }
299  return $username;
300  }
301 
309  public static function mergeFieldInfo( array $reqs ) {
310  $merged = [];
311 
312  // fields that are required by some primary providers but not others are not actually required
313  $primaryRequests = array_filter( $reqs, function ( $req ) {
314  return $req->required === AuthenticationRequest::PRIMARY_REQUIRED;
315  } );
316  $sharedRequiredPrimaryFields = array_reduce( $primaryRequests, function ( $shared, $req ) {
317  $required = array_keys( array_filter( $req->getFieldInfo(), function ( $options ) {
318  return empty( $options['optional'] );
319  } ) );
320  if ( $shared === null ) {
321  return $required;
322  } else {
323  return array_intersect( $shared, $required );
324  }
325  }, null );
326 
327  foreach ( $reqs as $req ) {
328  $info = $req->getFieldInfo();
329  if ( !$info ) {
330  continue;
331  }
332 
333  foreach ( $info as $name => $options ) {
334  if (
335  // If the request isn't required, its fields aren't required either.
336  $req->required === self::OPTIONAL
337  // If there is a primary not requiring this field, no matter how many others do,
338  // authentication can proceed without it.
339  || $req->required === self::PRIMARY_REQUIRED
340  && !in_array( $name, $sharedRequiredPrimaryFields, true )
341  ) {
342  $options['optional'] = true;
343  } else {
344  $options['optional'] = !empty( $options['optional'] );
345  }
346 
347  $options['sensitive'] = !empty( $options['sensitive'] );
348  $type = $options['type'];
349 
350  if ( !array_key_exists( $name, $merged ) ) {
351  $merged[$name] = $options;
352  } elseif ( $merged[$name]['type'] !== $type ) {
353  throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
354  "\"{$merged[$name]['type']}\" vs \"$type\""
355  );
356  } else {
357  if ( isset( $options['options'] ) ) {
358  if ( isset( $merged[$name]['options'] ) ) {
359  $merged[$name]['options'] += $options['options'];
360  } else {
361  // @codeCoverageIgnoreStart
362  $merged[$name]['options'] = $options['options'];
363  // @codeCoverageIgnoreEnd
364  }
365  }
366 
367  $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
368  $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive'];
369 
370  // No way to merge 'value', 'image', 'help', or 'label', so just use
371  // the value from the first request.
372  }
373  }
374  }
375 
376  return $merged;
377  }
378 
384  public static function __set_state( $data ) {
385  // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
386  $ret = new static();
387  foreach ( $data as $k => $v ) {
388  $ret->$k = $v;
389  }
390  return $ret;
391  }
392 }
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
static mergeFieldInfo(array $reqs)
Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
The Message class provides methods which fulfil two basic services:
Definition: Message.php:162
static __set_state( $data)
Implementing this mainly for use from the unit tests.
static loadRequestsFromSubmission(array $reqs, array $data)
Update a set of requests with form submit data, discarding ones that fail.
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
getFieldInfo()
Fetch input field info.
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...
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
const REQUIRED
Indicates that the request is required for authentication to proceed.
getMetadata()
Returns metadata about this request.
string null $returnToUrl
Return-to URL, in case of redirect.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
string null $action
The AuthManager::ACTION_* constant this request was created to be used for.
This is a value object for authentication requests.
loadFromSubmission(array $data)
Initialize form submitted form data.
describeCredentials()
Describe the credentials represented by this request.