Completed
Branch master (726f70)
by
unknown
25:29
created

ApiAuthManagerHelper::logAuthenticationResult()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
nc 4
nop 2
dl 0
loc 18
rs 9.2
c 1
b 0
f 0
1
<?php
2
/**
3
 * Copyright © 2016 Brad Jorsch <[email protected]>
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
 * @since 1.27
22
 */
23
24
use MediaWiki\Auth\AuthManager;
25
use MediaWiki\Auth\AuthenticationRequest;
26
use MediaWiki\Auth\AuthenticationResponse;
27
use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
28
use MediaWiki\Logger\LoggerFactory;
29
30
/**
31
 * Helper class for AuthManager-using API modules. Intended for use via
32
 * composition.
33
 *
34
 * @ingroup API
35
 */
36
class ApiAuthManagerHelper {
37
38
	/** @var ApiBase API module, for context and parameters */
39
	private $module;
40
41
	/** @var string Message output format */
42
	private $messageFormat;
43
44
	/**
45
	 * @param ApiBase $module API module, for context and parameters
46
	 */
47
	public function __construct( ApiBase $module ) {
48
		$this->module = $module;
49
50
		$params = $module->extractRequestParams();
51
		$this->messageFormat = isset( $params['messageformat'] ) ? $params['messageformat'] : 'wikitext';
52
	}
53
54
	/**
55
	 * Static version of the constructor, for chaining
56
	 * @param ApiBase $module API module, for context and parameters
57
	 * @return ApiAuthManagerHelper
58
	 */
59
	public static function newForModule( ApiBase $module ) {
60
		return new self( $module );
61
	}
62
63
	/**
64
	 * Format a message for output
65
	 * @param array &$res Result array
66
	 * @param string $key Result key
67
	 * @param Message $message
68
	 */
69
	private function formatMessage( array &$res, $key, Message $message ) {
70
		switch ( $this->messageFormat ) {
71
			case 'none':
72
				break;
73
74
			case 'wikitext':
75
				$res[$key] = $message->setContext( $this->module )->text();
76
				break;
77
78
			case 'html':
79
				$res[$key] = $message->setContext( $this->module )->parseAsBlock();
80
				$res[$key] = Parser::stripOuterParagraph( $res[$key] );
81
				break;
82
83
			case 'raw':
84
				$res[$key] = [
85
					'key' => $message->getKey(),
86
					'params' => $message->getParams(),
87
				];
88
				break;
89
		}
90
	}
91
92
	/**
93
	 * Call $manager->securitySensitiveOperationStatus()
94
	 * @param string $operation Operation being checked.
95
	 * @throws UsageException
96
	 */
97
	public function securitySensitiveOperation( $operation ) {
98
		$status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
99
		switch ( $status ) {
100
			case AuthManager::SEC_OK:
101
				return;
102
103
			case AuthManager::SEC_REAUTH:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
104
				$this->module->dieUsage(
105
					'You have not authenticated recently in this session, please reauthenticate.', 'reauthenticate'
106
				);
107
108
			case AuthManager::SEC_FAIL:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
109
				$this->module->dieUsage(
110
					'This action is not available as your identify cannot be verified.', 'cannotreauthenticate'
111
				);
112
113
			default:
114
				throw new UnexpectedValueException( "Unknown status \"$status\"" );
115
		}
116
	}
117
118
	/**
119
	 * Filter out authentication requests by class name
120
	 * @param AuthenticationRequest[] $reqs Requests to filter
121
	 * @param string[] $blacklist Class names to remove
122
	 * @return AuthenticationRequest[]
123
	 */
124
	public static function blacklistAuthenticationRequests( array $reqs, array $blacklist ) {
125
		if ( $blacklist ) {
126
			$blacklist = array_flip( $blacklist );
127
			$reqs = array_filter( $reqs, function ( $req ) use ( $blacklist ) {
128
				return !isset( $blacklist[get_class( $req )] );
129
			} );
130
		}
131
		return $reqs;
132
	}
133
134
	/**
135
	 * Fetch and load the AuthenticationRequests for an action
136
	 * @param string $action One of the AuthManager::ACTION_* constants
137
	 * @return AuthenticationRequest[]
138
	 */
139
	public function loadAuthenticationRequests( $action ) {
140
		$params = $this->module->extractRequestParams();
141
142
		$manager = AuthManager::singleton();
143
		$reqs = $manager->getAuthenticationRequests( $action, $this->module->getUser() );
144
145
		// Filter requests, if requested to do so
146
		$wantedRequests = null;
147
		if ( isset( $params['requests'] ) ) {
148
			$wantedRequests = array_flip( $params['requests'] );
149
		} elseif ( isset( $params['request'] ) ) {
150
			$wantedRequests = [ $params['request'] => true ];
151
		}
152
		if ( $wantedRequests !== null ) {
153
			$reqs = array_filter( $reqs, function ( $req ) use ( $wantedRequests ) {
154
				return isset( $wantedRequests[$req->getUniqueId()] );
155
			} );
156
		}
157
158
		// Collect the fields for all the requests
159
		$fields = [];
160
		foreach ( $reqs as $req ) {
161
			$fields += (array)$req->getFieldInfo();
162
		}
163
164
		// Extract the request data for the fields and mark those request
165
		// parameters as used
166
		$data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
167
		$this->module->getMain()->markParamsUsed( array_keys( $data ) );
168
169
		return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
170
	}
171
172
	/**
173
	 * Format an AuthenticationResponse for return
174
	 * @param AuthenticationResponse $res
175
	 * @return array
176
	 */
177
	public function formatAuthenticationResponse( AuthenticationResponse $res ) {
178
		$params = $this->module->extractRequestParams();
0 ignored issues
show
Unused Code introduced by
$params is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
179
180
		$ret = [
181
			'status' => $res->status,
182
		];
183
184
		if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
185
			$ret['username'] = $res->username;
186
		}
187
188
		if ( $res->status === AuthenticationResponse::REDIRECT ) {
189
			$ret['redirecttarget'] = $res->redirectTarget;
190
			if ( $res->redirectApiData !== null ) {
191
				$ret['redirectdata'] = $res->redirectApiData;
192
			}
193
		}
194
195
		if ( $res->status === AuthenticationResponse::REDIRECT ||
196
			$res->status === AuthenticationResponse::UI ||
197
			$res->status === AuthenticationResponse::RESTART
198
		) {
199
			$ret += $this->formatRequests( $res->neededRequests );
200
		}
201
202
		if ( $res->status === AuthenticationResponse::FAIL ||
203
			$res->status === AuthenticationResponse::UI ||
204
			$res->status === AuthenticationResponse::RESTART
205
		) {
206
			$this->formatMessage( $ret, 'message', $res->message );
0 ignored issues
show
Bug introduced by
It seems like $res->message can be null; however, formatMessage() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
207
		}
208
209
		if ( $res->status === AuthenticationResponse::FAIL ||
210
			$res->status === AuthenticationResponse::RESTART
211
		) {
212
			$this->module->getRequest()->getSession()->set(
213
				'ApiAuthManagerHelper::createRequest',
214
				$res->createRequest
215
			);
216
			$ret['canpreservestate'] = $res->createRequest !== null;
217
		} else {
218
			$this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
219
		}
220
221
		return $ret;
222
	}
223
224
	/**
225
	 * Logs successful or failed authentication.
226
	 * @param string|AuthenticationResponse $result Response or error message
227
	 * @param string $event Event type (e.g. 'accountcreation')
228
	 */
229
	public function logAuthenticationResult( $event, $result ) {
230
		if ( is_string( $result ) ) {
231
			$status = Status::newFatal( $result );
232
		} elseif ( $result->status === AuthenticationResponse::PASS ) {
233
			$status = Status::newGood();
234
		} elseif ( $result->status === AuthenticationResponse::FAIL ) {
235
			$status = Status::newFatal( $result->message );
236
		} else {
237
			return;
238
		}
239
240
		$module = $this->module->getModuleName();
241
		LoggerFactory::getInstance( 'authmanager' )->info( "$module API attempt", [
242
			'event' => $event,
243
			'status' => $status,
244
			'module' => $module,
245
		] );
246
	}
247
248
	/**
249
	 * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
250
	 * @return CreateFromLoginAuthenticationRequest|null
251
	 */
252
	public function getPreservedRequest() {
253
		$ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
254
		return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
255
	}
256
257
	/**
258
	 * Format an array of AuthenticationRequests for return
259
	 * @param AuthenticationRequest[] $reqs
260
	 * @return array Will have a 'requests' key, and also 'fields' if $module's
261
	 *  params include 'mergerequestfields'.
262
	 */
263
	public function formatRequests( array $reqs ) {
264
		$params = $this->module->extractRequestParams();
265
		$mergeFields = !empty( $params['mergerequestfields'] );
266
267
		$ret = [ 'requests' => [] ];
268
		foreach ( $reqs as $req ) {
269
			$describe = $req->describeCredentials();
270
			$reqInfo = [
271
				'id' => $req->getUniqueId(),
272
				'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ],
273
			];
274
			switch ( $req->required ) {
275
				case AuthenticationRequest::OPTIONAL:
276
					$reqInfo['required'] = 'optional';
277
					break;
278
				case AuthenticationRequest::REQUIRED:
279
					$reqInfo['required'] = 'required';
280
					break;
281
				case AuthenticationRequest::PRIMARY_REQUIRED:
282
					$reqInfo['required'] = 'primary-required';
283
					break;
284
			}
285
			$this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
286
			$this->formatMessage( $reqInfo, 'account', $describe['account'] );
287
			if ( !$mergeFields ) {
288
				$reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
289
			}
290
			$ret['requests'][] = $reqInfo;
291
		}
292
293
		if ( $mergeFields ) {
294
			$fields = AuthenticationRequest::mergeFieldInfo( $reqs );
295
			$ret['fields'] = $this->formatFields( $fields );
296
		}
297
298
		return $ret;
299
	}
300
301
	/**
302
	 * Clean up a field array for output
303
	 * @param ApiBase $module For context and parameters 'mergerequestfields'
0 ignored issues
show
Bug introduced by
There is no parameter named $module. 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...
304
	 *  and 'messageformat'
305
	 * @param array $fields
306
	 * @return array
307
	 */
308
	private function formatFields( array $fields ) {
309
		static $copy = [
310
			'type' => true,
311
			'value' => true,
312
		];
313
314
		$module = $this->module;
315
		$retFields = [];
316
317
		foreach ( $fields as $name => $field ) {
318
			$ret = array_intersect_key( $field, $copy );
319
320
			if ( isset( $field['options'] ) ) {
321
				$ret['options'] = array_map( function ( $msg ) use ( $module ) {
322
					return $msg->setContext( $module )->plain();
323
				}, $field['options'] );
324
				ApiResult::setArrayType( $ret['options'], 'assoc' );
325
			}
326
			$this->formatMessage( $ret, 'label', $field['label'] );
327
			$this->formatMessage( $ret, 'help', $field['help'] );
328
			$ret['optional'] = !empty( $field['optional'] );
329
330
			$retFields[$name] = $ret;
331
		}
332
333
		ApiResult::setArrayType( $retFields, 'assoc' );
334
335
		return $retFields;
336
	}
337
338
	/**
339
	 * Fetch the standard parameters this helper recognizes
340
	 * @param string $action AuthManager action
341
	 * @param string $param... Parameters to use
0 ignored issues
show
Bug introduced by
There is no parameter named $param.... 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...
342
	 * @return array
343
	 */
344
	public static function getStandardParams( $action, $param /* ... */ ) {
345
		$params = [
346
			'requests' => [
347
				ApiBase::PARAM_TYPE => 'string',
348
				ApiBase::PARAM_ISMULTI => true,
349
				ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
350
			],
351
			'request' => [
352
				ApiBase::PARAM_TYPE => 'string',
353
				ApiBase::PARAM_REQUIRED => true,
354
				ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
355
			],
356
			'messageformat' => [
357
				ApiBase::PARAM_DFLT => 'wikitext',
358
				ApiBase::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
359
				ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
360
			],
361
			'mergerequestfields' => [
362
				ApiBase::PARAM_DFLT => false,
363
				ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
364
			],
365
			'preservestate' => [
366
				ApiBase::PARAM_DFLT => false,
367
				ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
368
			],
369
			'returnurl' => [
370
				ApiBase::PARAM_TYPE => 'string',
371
				ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
372
			],
373
			'continue' => [
374
				ApiBase::PARAM_DFLT => false,
375
				ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
376
			],
377
		];
378
379
		$ret = [];
380
		$wantedParams = func_get_args();
381
		array_shift( $wantedParams );
382
		foreach ( $wantedParams as $name ) {
383
			if ( isset( $params[$name] ) ) {
384
				$ret[$name] = $params[$name];
385
			}
386
		}
387
		return $ret;
388
	}
389
}
390