Completed
Branch master (1b8556)
by
unknown
26:56
created

AuthenticationRequest::mergeFieldInfo()   D

Complexity

Conditions 10
Paths 5

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 10
eloc 24
c 1
b 0
f 1
nc 5
nop 1
dl 0
loc 44
rs 4.8196

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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="image"> if 'image' is set, otherwise <input type="submit">
96
	 *       (uses 'label' as button text)
97
	 *     - hidden: Not visible to the user, but needs to be preserved for the next request
98
	 *     - null: No widget, just display the 'label' message.
99
	 *  - options: (array) Maps option values to Messages for the
100
	 *      'select' and 'multiselect' types.
101
	 *  - value: (string) Value (for 'null' and 'hidden') or default value (for other types).
102
	 *  - image: (string) URL of an image to use in connection with the input
103
	 *  - label: (Message) Text suitable for a label in an HTML form
104
	 *  - help: (Message) Text suitable as a description of what the field is
105
	 *  - optional: (bool) If set and truthy, the field may be left empty
106
	 *
107
	 * @return array As above
108
	 */
109
	abstract public function getFieldInfo();
110
111
	/**
112
	 * Returns metadata about this request.
113
	 *
114
	 * This is mainly for the benefit of API clients which need more detailed render hints
115
	 * than what's available through getFieldInfo(). Semantics are unspecified and left to the
116
	 * individual subclasses, but the contents of the array should be primitive types so that they
117
	 * can be transformed into JSON or similar formats.
118
	 *
119
	 * @return array A (possibly nested) array with primitive types
120
	 */
121
	public function getMetadata() {
122
		return [];
123
	}
124
125
	/**
126
	 * Initialize form submitted form data.
127
	 *
128
	 * Should always return false if self::getFieldInfo() returns an empty
129
	 * array.
130
	 *
131
	 * @param array $data Submitted data as an associative array
132
	 * @return bool Whether the request data was successfully loaded
133
	 */
134
	public function loadFromSubmission( array $data ) {
135
		$fields = array_filter( $this->getFieldInfo(), function ( $info ) {
136
			return $info['type'] !== 'null';
137
		} );
138
		if ( !$fields ) {
139
			return false;
140
		}
141
142
		foreach ( $fields as $field => $info ) {
143
			// Checkboxes and buttons are special. Depending on the method used
144
			// to populate $data, they might be unset meaning false or they
145
			// might be boolean. Further, image buttons might submit the
146
			// coordinates of the click rather than the expected value.
147
			if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
148
				$this->$field = isset( $data[$field] ) && $data[$field] !== false
149
					|| isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
150
				if ( !$this->$field && empty( $info['optional'] ) ) {
151
					return false;
152
				}
153
				continue;
154
			}
155
156
			// Multiselect are too, slightly
157
			if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
158
				$data[$field] = [];
159
			}
160
161
			if ( !isset( $data[$field] ) ) {
162
				return false;
163
			}
164
			if ( $data[$field] === '' || $data[$field] === [] ) {
165
				if ( empty( $info['optional'] ) ) {
166
					return false;
167
				}
168
			} else {
169
				switch ( $info['type'] ) {
170
					case 'select':
171
						if ( !isset( $info['options'][$data[$field]] ) ) {
172
							return false;
173
						}
174
						break;
175
176
					case 'multiselect':
177
						$data[$field] = (array)$data[$field];
178
						$allowed = array_keys( $info['options'] );
179
						if ( array_diff( $data[$field], $allowed ) !== [] ) {
180
							return false;
181
						}
182
						break;
183
				}
184
			}
185
186
			$this->$field = $data[$field];
187
		}
188
189
		return true;
190
	}
191
192
	/**
193
	 * Describe the credentials represented by this request
194
	 *
195
	 * This is used on requests returned by
196
	 * AuthenticationProvider::getAuthenticationRequests() for ACTION_LINK
197
	 * and ACTION_REMOVE and for requests returned in
198
	 * AuthenticationResponse::$linkRequest to create useful user interfaces.
199
	 *
200
	 * @return Message[] with the following keys:
201
	 *  - provider: A Message identifying the service that provides
202
	 *    the credentials, e.g. the name of the third party authentication
203
	 *    service.
204
	 *  - account: A Message identifying the credentials themselves,
205
	 *    e.g. the email address used with the third party authentication
206
	 *    service.
207
	 */
208
	public function describeCredentials() {
209
		return [
210
			'provider' => new \RawMessage( '$1', [ get_called_class() ] ),
211
			'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ),
212
		];
213
	}
214
215
	/**
216
	 * Update a set of requests with form submit data, discarding ones that fail
217
	 * @param AuthenticationRequest[] $reqs
218
	 * @param array $data
219
	 * @return AuthenticationRequest[]
220
	 */
221
	public static function loadRequestsFromSubmission( array $reqs, array $data ) {
222
		return array_values( array_filter( $reqs, function ( $req ) use ( $data ) {
223
			return $req->loadFromSubmission( $data );
224
		} ) );
225
	}
226
227
	/**
228
	 * Select a request by class name.
229
	 * @param AuthenticationRequest[] $reqs
230
	 * @param string $class Class name
231
	 * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given
232
	 *   class.
233
	 * @return AuthenticationRequest|null Returns null if there is not exactly
234
	 *  one matching request.
235
	 */
236
	public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
237
		$requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) {
238
			if ( $allowSubclasses ) {
239
				return is_a( $req, $class, false );
240
			} else {
241
				return get_class( $req ) === $class;
242
			}
243
		} );
244
		return count( $requests ) === 1 ? reset( $requests ) : null;
245
	}
246
247
	/**
248
	 * Get the username from the set of requests
249
	 *
250
	 * Only considers requests that have a "username" field.
251
	 *
252
	 * @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...
253
	 * @return string|null
254
	 * @throws \UnexpectedValueException If multiple different usernames are present.
255
	 */
256
	public static function getUsernameFromRequests( array $reqs ) {
257
		$username = null;
258
		$otherClass = null;
259
		foreach ( $reqs as $req ) {
260
			$info = $req->getFieldInfo();
261
			if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
262
				if ( $username === null ) {
263
					$username = $req->username;
264
					$otherClass = get_class( $req );
265
				} elseif ( $username !== $req->username ) {
266
					$requestClass = get_class( $req );
267
					throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
268
						. "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
269
				}
270
			}
271
		}
272
		return $username;
273
	}
274
275
	/**
276
	 * Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
277
	 * @param AuthenticationRequest[] $reqs
278
	 * @return array
279
	 * @throws \UnexpectedValueException If fields cannot be merged
280
	 */
281
	public static function mergeFieldInfo( array $reqs ) {
282
		$merged = [];
283
284
		foreach ( $reqs as $req ) {
285
			$info = $req->getFieldInfo();
286
			if ( !$info ) {
287
				continue;
288
			}
289
290
			foreach ( $info as $name => $options ) {
291
				if ( $req->required !== self::REQUIRED ) {
292
					// If the request isn't required, its fields aren't required either.
293
					$options['optional'] = true;
294
				} else {
295
					$options['optional'] = !empty( $options['optional'] );
296
				}
297
298
				if ( !array_key_exists( $name, $merged ) ) {
299
					$merged[$name] = $options;
300
				} elseif ( $merged[$name]['type'] !== $options['type'] ) {
301
					throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
302
						"\"{$merged[$name]['type']}\" vs \"{$options['type']}\""
303
					);
304
				} else {
305
					if ( isset( $options['options'] ) ) {
306
						if ( isset( $merged[$name]['options'] ) ) {
307
							$merged[$name]['options'] += $options['options'];
308
						} else {
309
							// @codeCoverageIgnoreStart
310
							$merged[$name]['options'] = $options['options'];
311
							// @codeCoverageIgnoreEnd
312
						}
313
					}
314
315
					$merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
316
317
					// No way to merge 'value', 'image', 'help', or 'label', so just use
318
					// the value from the first request.
319
				}
320
			}
321
		}
322
323
		return $merged;
324
	}
325
326
	/**
327
	 * Implementing this mainly for use from the unit tests.
328
	 * @param array $data
329
	 * @return AuthenticationRequest
330
	 */
331
	public static function __set_state( $data ) {
332
		$ret = new static();
333
		foreach ( $data as $k => $v ) {
334
			$ret->$k = $v;
335
		}
336
		return $ret;
337
	}
338
}
339