3 * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
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.
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.
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
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
;
31 * Helper class for AuthManager-using API modules. Intended for use via
36 class ApiAuthManagerHelper
{
38 /** @var ApiBase API module, for context and parameters */
41 /** @var string Message output format */
42 private $messageFormat;
45 * @param ApiBase $module API module, for context and parameters
47 public function __construct( ApiBase
$module ) {
48 $this->module
= $module;
50 $params = $module->extractRequestParams();
51 $this->messageFormat
= isset( $params['messageformat'] ) ?
$params['messageformat'] : 'wikitext';
55 * Static version of the constructor, for chaining
56 * @param ApiBase $module API module, for context and parameters
57 * @return ApiAuthManagerHelper
59 public static function newForModule( ApiBase
$module ) {
60 return new self( $module );
64 * Format a message for output
65 * @param array &$res Result array
66 * @param string $key Result key
67 * @param Message $message
69 private function formatMessage( array &$res, $key, Message
$message ) {
70 switch ( $this->messageFormat
) {
75 $res[$key] = $message->setContext( $this->module
)->text();
79 $res[$key] = $message->setContext( $this->module
)->parseAsBlock();
80 $res[$key] = Parser
::stripOuterParagraph( $res[$key] );
85 'key' => $message->getKey(),
86 'params' => $message->getParams(),
93 * Call $manager->securitySensitiveOperationStatus()
94 * @param string $operation Operation being checked.
95 * @throws UsageException
97 public function securitySensitiveOperation( $operation ) {
98 $status = AuthManager
::singleton()->securitySensitiveOperationStatus( $operation );
100 case AuthManager
::SEC_OK
:
103 case AuthManager
::SEC_REAUTH
:
104 $this->module
->dieUsage(
105 'You have not authenticated recently in this session, please reauthenticate.', 'reauthenticate'
108 case AuthManager
::SEC_FAIL
:
109 $this->module
->dieUsage(
110 'This action is not available as your identify cannot be verified.', 'cannotreauthenticate'
114 throw new UnexpectedValueException( "Unknown status \"$status\"" );
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[]
124 public static function blacklistAuthenticationRequests( array $reqs, array $blacklist ) {
126 $blacklist = array_flip( $blacklist );
127 $reqs = array_filter( $reqs, function ( $req ) use ( $blacklist ) {
128 return !isset( $blacklist[get_class( $req )] );
135 * Fetch and load the AuthenticationRequests for an action
136 * @param string $action One of the AuthManager::ACTION_* constants
137 * @return AuthenticationRequest[]
139 public function loadAuthenticationRequests( $action ) {
140 $params = $this->module
->extractRequestParams();
142 $manager = AuthManager
::singleton();
143 $reqs = $manager->getAuthenticationRequests( $action, $this->module
->getUser() );
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 ];
152 if ( $wantedRequests !== null ) {
153 $reqs = array_filter( $reqs, function ( $req ) use ( $wantedRequests ) {
154 return isset( $wantedRequests[$req->getUniqueId()] );
158 // Collect the fields for all the requests
161 foreach ( $reqs as $req ) {
162 $info = (array)$req->getFieldInfo();
164 $sensitive +
= array_filter( $info, function ( $opts ) {
165 return !empty( $opts['sensitive'] );
169 // Extract the request data for the fields and mark those request
170 // parameters as used
171 $data = array_intersect_key( $this->module
->getRequest()->getValues(), $fields );
172 $this->module
->getMain()->markParamsUsed( array_keys( $data ) );
176 $this->module
->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
177 } catch ( UsageException
$ex ) {
178 // Make this a warning for now, upgrade to an error in 1.29.
179 $this->module
->setWarning( $ex->getMessage() );
180 $this->module
->logFeatureUsage( $this->module
->getModuleName() . '-params-in-query-string' );
184 return AuthenticationRequest
::loadRequestsFromSubmission( $reqs, $data );
188 * Format an AuthenticationResponse for return
189 * @param AuthenticationResponse $res
192 public function formatAuthenticationResponse( AuthenticationResponse
$res ) {
193 $params = $this->module
->extractRequestParams();
196 'status' => $res->status
,
199 if ( $res->status
=== AuthenticationResponse
::PASS
&& $res->username
!== null ) {
200 $ret['username'] = $res->username
;
203 if ( $res->status
=== AuthenticationResponse
::REDIRECT
) {
204 $ret['redirecttarget'] = $res->redirectTarget
;
205 if ( $res->redirectApiData
!== null ) {
206 $ret['redirectdata'] = $res->redirectApiData
;
210 if ( $res->status
=== AuthenticationResponse
::REDIRECT ||
211 $res->status
=== AuthenticationResponse
::UI ||
212 $res->status
=== AuthenticationResponse
::RESTART
214 $ret +
= $this->formatRequests( $res->neededRequests
);
217 if ( $res->status
=== AuthenticationResponse
::FAIL ||
218 $res->status
=== AuthenticationResponse
::UI ||
219 $res->status
=== AuthenticationResponse
::RESTART
221 $this->formatMessage( $ret, 'message', $res->message
);
224 if ( $res->status
=== AuthenticationResponse
::FAIL ||
225 $res->status
=== AuthenticationResponse
::RESTART
227 $this->module
->getRequest()->getSession()->set(
228 'ApiAuthManagerHelper::createRequest',
231 $ret['canpreservestate'] = $res->createRequest
!== null;
233 $this->module
->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
240 * Logs successful or failed authentication.
241 * @param string|AuthenticationResponse $result Response or error message
242 * @param string $event Event type (e.g. 'accountcreation')
244 public function logAuthenticationResult( $event, $result ) {
245 if ( is_string( $result ) ) {
246 $status = Status
::newFatal( $result );
247 } elseif ( $result->status
=== AuthenticationResponse
::PASS
) {
248 $status = Status
::newGood();
249 } elseif ( $result->status
=== AuthenticationResponse
::FAIL
) {
250 $status = Status
::newFatal( $result->message
);
255 $module = $this->module
->getModuleName();
256 LoggerFactory
::getInstance( 'authevents' )->info( "$module API attempt", [
264 * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
265 * @return CreateFromLoginAuthenticationRequest|null
267 public function getPreservedRequest() {
268 $ret = $this->module
->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
269 return $ret instanceof CreateFromLoginAuthenticationRequest ?
$ret : null;
273 * Format an array of AuthenticationRequests for return
274 * @param AuthenticationRequest[] $reqs
275 * @return array Will have a 'requests' key, and also 'fields' if $module's
276 * params include 'mergerequestfields'.
278 public function formatRequests( array $reqs ) {
279 $params = $this->module
->extractRequestParams();
280 $mergeFields = !empty( $params['mergerequestfields'] );
282 $ret = [ 'requests' => [] ];
283 foreach ( $reqs as $req ) {
284 $describe = $req->describeCredentials();
286 'id' => $req->getUniqueId(),
287 'metadata' => $req->getMetadata() +
[ ApiResult
::META_TYPE
=> 'assoc' ],
289 switch ( $req->required
) {
290 case AuthenticationRequest
::OPTIONAL
:
291 $reqInfo['required'] = 'optional';
293 case AuthenticationRequest
::REQUIRED
:
294 $reqInfo['required'] = 'required';
296 case AuthenticationRequest
::PRIMARY_REQUIRED
:
297 $reqInfo['required'] = 'primary-required';
300 $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
301 $this->formatMessage( $reqInfo, 'account', $describe['account'] );
302 if ( !$mergeFields ) {
303 $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
305 $ret['requests'][] = $reqInfo;
308 if ( $mergeFields ) {
309 $fields = AuthenticationRequest
::mergeFieldInfo( $reqs );
310 $ret['fields'] = $this->formatFields( $fields );
317 * Clean up a field array for output
318 * @param ApiBase $module For context and parameters 'mergerequestfields'
319 * and 'messageformat'
320 * @param array $fields
323 private function formatFields( array $fields ) {
329 $module = $this->module
;
332 foreach ( $fields as $name => $field ) {
333 $ret = array_intersect_key( $field, $copy );
335 if ( isset( $field['options'] ) ) {
336 $ret['options'] = array_map( function ( $msg ) use ( $module ) {
337 return $msg->setContext( $module )->plain();
338 }, $field['options'] );
339 ApiResult
::setArrayType( $ret['options'], 'assoc' );
341 $this->formatMessage( $ret, 'label', $field['label'] );
342 $this->formatMessage( $ret, 'help', $field['help'] );
343 $ret['optional'] = !empty( $field['optional'] );
344 $ret['sensitive'] = !empty( $field['sensitive'] );
346 $retFields[$name] = $ret;
349 ApiResult
::setArrayType( $retFields, 'assoc' );
355 * Fetch the standard parameters this helper recognizes
356 * @param string $action AuthManager action
357 * @param string $param... Parameters to use
360 public static function getStandardParams( $action, $param /* ... */ ) {
363 ApiBase
::PARAM_TYPE
=> 'string',
364 ApiBase
::PARAM_ISMULTI
=> true,
365 ApiBase
::PARAM_HELP_MSG
=> [ 'api-help-authmanagerhelper-requests', $action ],
368 ApiBase
::PARAM_TYPE
=> 'string',
369 ApiBase
::PARAM_REQUIRED
=> true,
370 ApiBase
::PARAM_HELP_MSG
=> [ 'api-help-authmanagerhelper-request', $action ],
373 ApiBase
::PARAM_DFLT
=> 'wikitext',
374 ApiBase
::PARAM_TYPE
=> [ 'html', 'wikitext', 'raw', 'none' ],
375 ApiBase
::PARAM_HELP_MSG
=> 'api-help-authmanagerhelper-messageformat',
377 'mergerequestfields' => [
378 ApiBase
::PARAM_DFLT
=> false,
379 ApiBase
::PARAM_HELP_MSG
=> 'api-help-authmanagerhelper-mergerequestfields',
382 ApiBase
::PARAM_DFLT
=> false,
383 ApiBase
::PARAM_HELP_MSG
=> 'api-help-authmanagerhelper-preservestate',
386 ApiBase
::PARAM_TYPE
=> 'string',
387 ApiBase
::PARAM_HELP_MSG
=> 'api-help-authmanagerhelper-returnurl',
390 ApiBase
::PARAM_DFLT
=> false,
391 ApiBase
::PARAM_HELP_MSG
=> 'api-help-authmanagerhelper-continue',
396 $wantedParams = func_get_args();
397 array_shift( $wantedParams );
398 foreach ( $wantedParams as $name ) {
399 if ( isset( $params[$name] ) ) {
400 $ret[$name] = $params[$name];