Completed
Branch master (227f0c)
by
unknown
30:54
created

AuthenticationRequest::getUniqueId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Authentication request value object
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Auth
22
 */
23
24
namespace MediaWiki\Auth;
25
26
use Message;
27
28
/**
29
 * This is a value object for authentication requests.
30
 *
31
 * An AuthenticationRequest represents a set of form fields that are needed on
32
 * and provided from the login, account creation, or password change forms.
33
 *
34
 * @ingroup Auth
35
 * @since 1.27
36
 */
37
abstract class AuthenticationRequest {
38
39
	/** Indicates that the request is not required for authentication to proceed. */
40
	const OPTIONAL = 0;
41
42
	/** Indicates that the request is required for authentication to proceed. */
43
	const REQUIRED = 1;
44
45
	/** Indicates that the request is required by a primary authentication
46
	 * provdier, but other primary authentication providers do not require it. */
47
	const PRIMARY_REQUIRED = 2;
48
49
	/** @var string|null The AuthManager::ACTION_* constant this request was
50
	 * created to be used for. The *_CONTINUE constants are not used here, the
51
	 * corresponding "begin" constant is used instead.
52
	 */
53
	public $action = null;
54
55
	/** @var int For login, continue, and link actions, one of self::OPTIONAL,
56
	 * self::REQUIRED, or self::PRIMARY_REQUIRED */
57
	public $required = self::REQUIRED;
58
59
	/** @var string|null Return-to URL, in case of redirect */
60
	public $returnToUrl = null;
61
62
	/** @var string|null Username. May not be used by all subclasses. */
63
	public $username = null;
64
65
	/**
66
	 * Supply a unique key for deduplication
67
	 *
68
	 * When the AuthenticationRequests instances returned by the providers are
69
	 * merged, the value returned here is used for keeping only one copy of
70
	 * duplicate requests.
71
	 *
72
	 * Subclasses should override this if multiple distinct instances would
73
	 * make sense, i.e. the request class has internal state of some sort.
74
	 *
75
	 * This value might be exposed to the user in web forms so it should not
76
	 * contain private information.
77
	 *
78
	 * @return string
79
	 */
80
	public function getUniqueId() {
81
		return get_called_class();
82
	}
83
84
	/**
85
	 * Fetch input field info
86
	 *
87
	 * The field info is an associative array mapping field names to info
88
	 * arrays. The info arrays have the following keys:
89
	 *  - type: (string) Type of input. Types and equivalent HTML widgets are:
90
	 *     - string: <input type="text">
91
	 *     - password: <input type="password">
92
	 *     - select: <select>
93
	 *     - checkbox: <input type="checkbox">
94
	 *     - multiselect: More a grid of checkboxes than <select multi>
95
	 *     - button: <input type="submit"> (uses 'label' as button text)
96
	 *     - hidden: Not visible to the user, but needs to be preserved for the next request
97
	 *     - null: No widget, just display the 'label' message.
98
	 *  - options: (array) Maps option values to Messages for the
99
	 *      'select' and 'multiselect' types.
100
	 *  - value: (string) Value (for 'null' and 'hidden') or default value (for other types).
101
	 *  - label: (Message) Text suitable for a label in an HTML form
102
	 *  - help: (Message) Text suitable as a description of what the field is
103
	 *  - optional: (bool) If set and truthy, the field may be left empty
104
	 *
105
	 * @return array As above
106
	 */
107
	abstract public function getFieldInfo();
108
109
	/**
110
	 * Returns metadata about this request.
111
	 *
112
	 * This is mainly for the benefit of API clients which need more detailed render hints
113
	 * than what's available through getFieldInfo(). Semantics are unspecified and left to the
114
	 * individual subclasses, but the contents of the array should be primitive types so that they
115
	 * can be transformed into JSON or similar formats.
116
	 *
117
	 * @return array A (possibly nested) array with primitive types
118
	 */
119
	public function getMetadata() {
120
		return [];
121
	}
122
123
	/**
124
	 * Initialize form submitted form data.
125
	 *
126
	 * Should always return false if self::getFieldInfo() returns an empty
127
	 * array.
128
	 *
129
	 * @param array $data Submitted data as an associative array
130
	 * @return bool Whether the request data was successfully loaded
131
	 */
132
	public function loadFromSubmission( array $data ) {
133
		$fields = array_filter( $this->getFieldInfo(), function ( $info ) {
134
			return $info['type'] !== 'null';
135
		} );
136
		if ( !$fields ) {
137
			return false;
138
		}
139
140
		foreach ( $fields as $field => $info ) {
141
			// Checkboxes and buttons are special. Depending on the method used
142
			// to populate $data, they might be unset meaning false or they
143
			// might be boolean. Further, image buttons might submit the
144
			// coordinates of the click rather than the expected value.
145
			if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
146
				$this->$field = isset( $data[$field] ) && $data[$field] !== false
147
					|| isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
148
				if ( !$this->$field && empty( $info['optional'] ) ) {
149
					return false;
150
				}
151
				continue;
152
			}
153
154
			// Multiselect are too, slightly
155
			if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
156
				$data[$field] = [];
157
			}
158
159
			if ( !isset( $data[$field] ) ) {
160
				return false;
161
			}
162
			if ( $data[$field] === '' || $data[$field] === [] ) {
163
				if ( empty( $info['optional'] ) ) {
164
					return false;
165
				}
166
			} else {
167
				switch ( $info['type'] ) {
168
					case 'select':
169
						if ( !isset( $info['options'][$data[$field]] ) ) {
170
							return false;
171
						}
172
						break;
173
174
					case 'multiselect':
175
						$data[$field] = (array)$data[$field];
176
						$allowed = array_keys( $info['options'] );
177
						if ( array_diff( $data[$field], $allowed ) !== [] ) {
178
							return false;
179
						}
180
						break;
181
				}
182
			}
183
184
			$this->$field = $data[$field];
185
		}
186
187
		return true;
188
	}
189
190
	/**
191
	 * Describe the credentials represented by this request
192
	 *
193
	 * This is used on requests returned by
194
	 * AuthenticationProvider::getAuthenticationRequests() for ACTION_LINK
195
	 * and ACTION_REMOVE and for requests returned in
196
	 * AuthenticationResponse::$linkRequest to create useful user interfaces.
197
	 *
198
	 * @return Message[] with the following keys:
199
	 *  - provider: A Message identifying the service that provides
200
	 *    the credentials, e.g. the name of the third party authentication
201
	 *    service.
202
	 *  - account: A Message identifying the credentials themselves,
203
	 *    e.g. the email address used with the third party authentication
204
	 *    service.
205
	 */
206
	public function describeCredentials() {
207
		return [
208
			'provider' => new \RawMessage( '$1', [ get_called_class() ] ),
209
			'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ),
210
		];
211
	}
212
213
	/**
214
	 * Update a set of requests with form submit data, discarding ones that fail
215
	 * @param AuthenticationRequest[] $reqs
216
	 * @param array $data
217
	 * @return AuthenticationRequest[]
218
	 */
219
	public static function loadRequestsFromSubmission( array $reqs, array $data ) {
220
		return array_values( array_filter( $reqs, function ( $req ) use ( $data ) {
221
			return $req->loadFromSubmission( $data );
222
		} ) );
223
	}
224
225
	/**
226
	 * Select a request by class name.
227
	 * @param AuthenticationRequest[] $reqs
228
	 * @param string $class Class name
229
	 * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given
230
	 *   class.
231
	 * @return AuthenticationRequest|null Returns null if there is not exactly
232
	 *  one matching request.
233
	 */
234
	public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
235
		$requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) {
236
			if ( $allowSubclasses ) {
237
				return is_a( $req, $class, false );
238
			} else {
239
				return get_class( $req ) === $class;
240
			}
241
		} );
242
		return count( $requests ) === 1 ? reset( $requests ) : null;
243
	}
244
245
	/**
246
	 * Get the username from the set of requests
247
	 *
248
	 * Only considers requests that have a "username" field.
249
	 *
250
	 * @param AuthenticationRequest[] $requests
0 ignored issues
show
Bug introduced by
There is no parameter named $requests. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
251
	 * @return string|null
252
	 * @throws \UnexpectedValueException If multiple different usernames are present.
253
	 */
254
	public static function getUsernameFromRequests( array $reqs ) {
255
		$username = null;
256
		$otherClass = null;
257
		foreach ( $reqs as $req ) {
258
			$info = $req->getFieldInfo();
259
			if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
260
				if ( $username === null ) {
261
					$username = $req->username;
262
					$otherClass = get_class( $req );
263
				} elseif ( $username !== $req->username ) {
264
					$requestClass = get_class( $req );
265
					throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
266
						. "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
267
				}
268
			}
269
		}
270
		return $username;
271
	}
272
273
	/**
274
	 * Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
275
	 * @param AuthenticationRequest[] $reqs
276
	 * @return array
277
	 * @throws \UnexpectedValueException If fields cannot be merged
278
	 */
279
	public static function mergeFieldInfo( array $reqs ) {
280
		$merged = [];
281
282
		// fields that are required by some primary providers but not others are not actually required
283
		$primaryRequests = array_filter( $reqs, function ( $req ) {
284
			return $req->required === AuthenticationRequest::PRIMARY_REQUIRED;
285
		} );
286
		$sharedRequiredPrimaryFields = array_reduce( $primaryRequests, function ( $shared, $req ) {
287
			$required = array_keys( array_filter( $req->getFieldInfo(), function ( $options ) {
288
				return empty( $options['optional'] );
289
			} ) );
290
			if ( $shared === null ) {
291
				return $required;
292
			} else {
293
				return array_intersect( $shared, $required );
294
			}
295
		}, null );
296
297
		foreach ( $reqs as $req ) {
298
			$info = $req->getFieldInfo();
299
			if ( !$info ) {
300
				continue;
301
			}
302
303
			foreach ( $info as $name => $options ) {
304
				if (
305
					// If the request isn't required, its fields aren't required either.
306
					$req->required === self::OPTIONAL
307
					// If there is a primary not requiring this field, no matter how many others do,
308
					// authentication can proceed without it.
309
					|| $req->required === self::PRIMARY_REQUIRED
310
						&& !in_array( $name, $sharedRequiredPrimaryFields, true )
311
				) {
312
					$options['optional'] = true;
313
				} else {
314
					$options['optional'] = !empty( $options['optional'] );
315
				}
316
317
				if ( !array_key_exists( $name, $merged ) ) {
318
					$merged[$name] = $options;
319
				} elseif ( $merged[$name]['type'] !== $options['type'] ) {
320
					throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
321
						"\"{$merged[$name]['type']}\" vs \"{$options['type']}\""
322
					);
323
				} else {
324
					if ( isset( $options['options'] ) ) {
325
						if ( isset( $merged[$name]['options'] ) ) {
326
							$merged[$name]['options'] += $options['options'];
327
						} else {
328
							// @codeCoverageIgnoreStart
329
							$merged[$name]['options'] = $options['options'];
330
							// @codeCoverageIgnoreEnd
331
						}
332
					}
333
334
					$merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
335
336
					// No way to merge 'value', 'image', 'help', or 'label', so just use
337
					// the value from the first request.
338
				}
339
			}
340
		}
341
342
		return $merged;
343
	}
344
345
	/**
346
	 * Implementing this mainly for use from the unit tests.
347
	 * @param array $data
348
	 * @return AuthenticationRequest
349
	 */
350
	public static function __set_state( $data ) {
351
		$ret = new static();
352
		foreach ( $data as $k => $v ) {
353
			$ret->$k = $v;
354
		}
355
		return $ret;
356
	}
357
}
358