MediaWiki REL1_37
AuthenticationRequest.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Auth;
25
26use Message;
27
38abstract class AuthenticationRequest {
39
41 public const OPTIONAL = 0;
42
47 public const REQUIRED = 1;
48
53 public const PRIMARY_REQUIRED = 2;
54
59 public $action = null;
60
65
67 public $returnToUrl = null;
68
72 public $username = null;
73
90 public function getUniqueId() {
91 return get_called_class();
92 }
93
129 abstract public function getFieldInfo();
130
142 public function getMetadata() {
143 return [];
144 }
145
159 public function loadFromSubmission( array $data ) {
160 $fields = array_filter( $this->getFieldInfo(), static function ( $info ) {
161 return $info['type'] !== 'null';
162 } );
163 if ( !$fields ) {
164 return false;
165 }
166
167 foreach ( $fields as $field => $info ) {
168 // Checkboxes and buttons are special. Depending on the method used
169 // to populate $data, they might be unset meaning false or they
170 // might be boolean. Further, image buttons might submit the
171 // coordinates of the click rather than the expected value.
172 if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
173 $this->$field = isset( $data[$field] ) && $data[$field] !== false
174 || isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
175 if ( !$this->$field && empty( $info['optional'] ) ) {
176 return false;
177 }
178 continue;
179 }
180
181 // Multiselect are too, slightly
182 if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
183 $data[$field] = [];
184 }
185
186 if ( !isset( $data[$field] ) ) {
187 return false;
188 }
189 if ( $data[$field] === '' || $data[$field] === [] ) {
190 if ( empty( $info['optional'] ) ) {
191 return false;
192 }
193 } else {
194 switch ( $info['type'] ) {
195 case 'select':
196 if ( !isset( $info['options'][$data[$field]] ) ) {
197 return false;
198 }
199 break;
200
201 case 'multiselect':
202 $data[$field] = (array)$data[$field];
203 $allowed = array_keys( $info['options'] );
204 if ( array_diff( $data[$field], $allowed ) !== [] ) {
205 return false;
206 }
207 break;
208 }
209 }
210
211 $this->$field = $data[$field];
212 }
213
214 return true;
215 }
216
235 public function describeCredentials() {
236 return [
237 'provider' => new \RawMessage( '$1', [ get_called_class() ] ),
238 'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ),
239 ];
240 }
241
249 public static function loadRequestsFromSubmission( array $reqs, array $data ) {
250 $result = [];
251 foreach ( $reqs as $req ) {
252 if ( $req->loadFromSubmission( $data ) ) {
253 $result[] = $req;
254 }
255 }
256 return $result;
257 }
258
272 public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
273 $requests = array_filter( $reqs, static function ( $req ) use ( $class, $allowSubclasses ) {
274 if ( $allowSubclasses ) {
275 return is_a( $req, $class, false );
276 } else {
277 return get_class( $req ) === $class;
278 }
279 } );
280 return count( $requests ) === 1 ? reset( $requests ) : null;
281 }
282
292 public static function getUsernameFromRequests( array $reqs ) {
293 $username = null;
294 $otherClass = null;
295 foreach ( $reqs as $req ) {
296 $info = $req->getFieldInfo();
297 if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
298 if ( $username === null ) {
299 $username = $req->username;
300 $otherClass = get_class( $req );
301 } elseif ( $username !== $req->username ) {
302 $requestClass = get_class( $req );
303 throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
304 . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
305 }
306 }
307 }
308 return $username;
309 }
310
317 public static function mergeFieldInfo( array $reqs ) {
318 $merged = [];
319
320 // fields that are required by some primary providers but not others are not actually required
321 $sharedRequiredPrimaryFields = null;
322 foreach ( $reqs as $req ) {
323 if ( $req->required !== self::PRIMARY_REQUIRED ) {
324 continue;
325 }
326 $required = [];
327 foreach ( $req->getFieldInfo() as $fieldName => $options ) {
328 if ( empty( $options['optional'] ) ) {
329 $required[] = $fieldName;
330 }
331 }
332 if ( $sharedRequiredPrimaryFields === null ) {
333 $sharedRequiredPrimaryFields = $required;
334 } else {
335 $sharedRequiredPrimaryFields = array_intersect( $sharedRequiredPrimaryFields, $required );
336 }
337 }
338
339 foreach ( $reqs as $req ) {
340 $info = $req->getFieldInfo();
341 if ( !$info ) {
342 continue;
343 }
344
345 foreach ( $info as $name => $options ) {
346 if (
347 // If the request isn't required, its fields aren't required either.
348 $req->required === self::OPTIONAL
349 // If there is a primary not requiring this field, no matter how many others do,
350 // authentication can proceed without it.
351 || $req->required === self::PRIMARY_REQUIRED
352 && !in_array( $name, $sharedRequiredPrimaryFields, true )
353 ) {
354 $options['optional'] = true;
355 } else {
356 $options['optional'] = !empty( $options['optional'] );
357 }
358
359 $options['sensitive'] = !empty( $options['sensitive'] );
360 $type = $options['type'];
361
362 if ( !array_key_exists( $name, $merged ) ) {
363 $merged[$name] = $options;
364 } elseif ( $merged[$name]['type'] !== $type ) {
365 throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
366 "\"{$merged[$name]['type']}\" vs \"$type\""
367 );
368 } else {
369 if ( isset( $options['options'] ) ) {
370 if ( isset( $merged[$name]['options'] ) ) {
371 $merged[$name]['options'] += $options['options'];
372 } else {
373 // @codeCoverageIgnoreStart
374 $merged[$name]['options'] = $options['options'];
375 // @codeCoverageIgnoreEnd
376 }
377 }
378
379 $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
380 $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive'];
381
382 // No way to merge 'value', 'image', 'help', or 'label', so just use
383 // the value from the first request.
384 }
385 }
386 }
387
388 return $merged;
389 }
390
396 public static function __set_state( $data ) {
397 // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
398 $ret = new static();
399 foreach ( $data as $k => $v ) {
400 $ret->$k = $v;
401 }
402 return $ret;
403 }
404}
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.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:138