MediaWiki REL1_34
AuthenticationRequest.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Auth;
25
26use Message;
27
37abstract class AuthenticationRequest {
38
40 const OPTIONAL = 0;
41
46 const REQUIRED = 1;
47
53
58 public $action = null;
59
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}
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 provides methods which fulfil two basic services:
Definition Message.php:162