Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 187
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiAuthManagerHelper
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 12
2652
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 newForModule
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatMessage
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 securitySensitiveOperation
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 blacklistAuthenticationRequests
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 loadAuthenticationRequests
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 formatAuthenticationResponse
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
182
 logAuthenticationResult
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getPreservedRequest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 formatRequests
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
56
 formatFields
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 getStandardParams
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Copyright © 2016 Wikimedia Foundation and contributors
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @since 1.27
8 */
9
10namespace MediaWiki\Api;
11
12use MediaWiki\Auth\AuthenticationRequest;
13use MediaWiki\Auth\AuthenticationResponse;
14use MediaWiki\Auth\AuthManager;
15use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
16use MediaWiki\Logger\LoggerFactory;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Message\Message;
19use MediaWiki\Parser\Parser;
20use MediaWiki\User\UserIdentity;
21use MediaWiki\User\UserIdentityUtils;
22use UnexpectedValueException;
23use Wikimedia\ParamValidator\ParamValidator;
24
25/**
26 * Helper class for AuthManager-using API modules. Intended for use via
27 * composition.
28 *
29 * @ingroup API
30 */
31class ApiAuthManagerHelper {
32
33    /** @var ApiBase API module, for context and parameters */
34    private $module;
35
36    /** @var string Message output format */
37    private $messageFormat;
38
39    private AuthManager $authManager;
40
41    private UserIdentityUtils $identityUtils;
42
43    /**
44     * @param ApiBase $module API module, for context and parameters
45     * @param AuthManager|null $authManager
46     * @param UserIdentityUtils|null $identityUtils
47     */
48    public function __construct(
49        ApiBase $module,
50        ?AuthManager $authManager = null,
51        ?UserIdentityUtils $identityUtils = null
52    ) {
53        $this->module = $module;
54
55        $params = $module->extractRequestParams();
56        $this->messageFormat = $params['messageformat'] ?? 'wikitext';
57        $this->authManager = $authManager ?? MediaWikiServices::getInstance()->getAuthManager();
58        // TODO: inject this as currently it's always taken from container
59        $this->identityUtils = $identityUtils ?? MediaWikiServices::getInstance()->getUserIdentityUtils();
60    }
61
62    /**
63     * Static version of the constructor, for chaining
64     * @param ApiBase $module API module, for context and parameters
65     * @param AuthManager|null $authManager
66     * @return ApiAuthManagerHelper
67     */
68    public static function newForModule( ApiBase $module, ?AuthManager $authManager = null ) {
69        return new self( $module, $authManager );
70    }
71
72    /**
73     * Format a message for output
74     * @param array &$res Result array
75     * @param string $key Result key
76     * @param Message $message
77     */
78    private function formatMessage( array &$res, $key, Message $message ) {
79        switch ( $this->messageFormat ) {
80            case 'none':
81                break;
82
83            case 'wikitext':
84                $res[$key] = $message->setContext( $this->module )->text();
85                break;
86
87            case 'html':
88                $res[$key] = $message->setContext( $this->module )->parseAsBlock();
89                $res[$key] = Parser::stripOuterParagraph( $res[$key] );
90                break;
91
92            case 'raw':
93                $params = $message->getParams();
94                $res[$key] = [
95                    'key' => $message->getKey(),
96                    'params' => $params,
97                ];
98                ApiResult::setIndexedTagName( $params, 'param' );
99                break;
100        }
101    }
102
103    /**
104     * Call $manager->securitySensitiveOperationStatus()
105     * @param string $operation Operation being checked.
106     * @throws ApiUsageException
107     */
108    public function securitySensitiveOperation( $operation ) {
109        $status = $this->authManager->securitySensitiveOperationStatus( $operation );
110        switch ( $status ) {
111            case AuthManager::SEC_OK:
112                return;
113
114            case AuthManager::SEC_REAUTH:
115                $this->module->dieWithError( 'apierror-reauthenticate' );
116                // dieWithError prevents continuation
117
118            case AuthManager::SEC_FAIL:
119                $this->module->dieWithError( 'apierror-cannotreauthenticate' );
120                // dieWithError prevents continuation
121
122            default:
123                throw new UnexpectedValueException( "Unknown status \"$status\"" );
124        }
125    }
126
127    /**
128     * Filter out authentication requests by class name
129     * @param AuthenticationRequest[] $reqs Requests to filter
130     * @param string[] $remove Class names to remove
131     * @return AuthenticationRequest[]
132     */
133    public static function blacklistAuthenticationRequests( array $reqs, array $remove ) {
134        if ( $remove ) {
135            $remove = array_fill_keys( $remove, true );
136            $reqs = array_filter( $reqs, static function ( $req ) use ( $remove ) {
137                return !isset( $remove[get_class( $req )] );
138            } );
139        }
140        return $reqs;
141    }
142
143    /**
144     * Fetch and load the AuthenticationRequests for an action
145     * @param string $action One of the AuthManager::ACTION_* constants
146     * @return AuthenticationRequest[]
147     */
148    public function loadAuthenticationRequests( $action ) {
149        $params = $this->module->extractRequestParams();
150
151        $reqs = $this->authManager->getAuthenticationRequests( $action, $this->module->getUser() );
152
153        // Filter requests, if requested to do so
154        $wantedRequests = null;
155        if ( isset( $params['requests'] ) ) {
156            $wantedRequests = array_fill_keys( $params['requests'], true );
157        } elseif ( isset( $params['request'] ) ) {
158            $wantedRequests = [ $params['request'] => true ];
159        }
160        if ( $wantedRequests !== null ) {
161            $reqs = array_filter(
162                $reqs,
163                static function ( AuthenticationRequest $req ) use ( $wantedRequests ) {
164                    return isset( $wantedRequests[$req->getUniqueId()] );
165                }
166            );
167        }
168
169        // Collect the fields for all the requests
170        $fields = [];
171        $sensitive = [];
172        foreach ( $reqs as $req ) {
173            $info = (array)$req->getFieldInfo();
174            $fields += $info;
175            $sensitive += array_filter( $info, static function ( $opts ) {
176                return !empty( $opts['sensitive'] );
177            } );
178        }
179
180        // Extract the request data for the fields and mark those request
181        // parameters as used
182        $data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
183        $this->module->getMain()->markParamsUsed( array_keys( $data ) );
184
185        if ( $sensitive ) {
186            $this->module->getMain()->markParamsSensitive( array_keys( $sensitive ) );
187            $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
188        }
189
190        return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
191    }
192
193    /**
194     * Format an AuthenticationResponse for return
195     * @param AuthenticationResponse $res
196     * @return array
197     */
198    public function formatAuthenticationResponse( AuthenticationResponse $res ) {
199        $ret = [
200            'status' => $res->status,
201        ];
202
203        if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
204            $ret['username'] = $res->username;
205        }
206
207        if ( $res->status === AuthenticationResponse::REDIRECT ) {
208            $ret['redirecttarget'] = $res->redirectTarget;
209            if ( $res->redirectApiData !== null ) {
210                $ret['redirectdata'] = $res->redirectApiData;
211            }
212        }
213
214        if ( $res->status === AuthenticationResponse::REDIRECT ||
215            $res->status === AuthenticationResponse::UI ||
216            $res->status === AuthenticationResponse::RESTART
217        ) {
218            $ret += $this->formatRequests( $res->neededRequests );
219        }
220
221        if ( $res->status === AuthenticationResponse::FAIL ||
222            $res->status === AuthenticationResponse::UI ||
223            $res->status === AuthenticationResponse::RESTART
224        ) {
225            $this->formatMessage( $ret, 'message', $res->message );
226            $ret['messagecode'] = ApiMessage::create( $res->message )->getApiCode();
227        }
228
229        if ( $res->status === AuthenticationResponse::FAIL ||
230            $res->status === AuthenticationResponse::RESTART
231        ) {
232            $this->module->getRequest()->getSession()->set(
233                'ApiAuthManagerHelper::createRequest',
234                $res->createRequest
235            );
236            $ret['canpreservestate'] = $res->createRequest !== null;
237        } else {
238            $this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
239        }
240
241        return $ret;
242    }
243
244    /**
245     * Logs successful or failed authentication.
246     * @param string $event Event type (e.g. 'accountcreation')
247     * @param UserIdentity $performer
248     * @param AuthenticationResponse $result Response or error message
249     */
250    public function logAuthenticationResult( $event, UserIdentity $performer, AuthenticationResponse $result ) {
251        if ( !in_array( $result->status, [ AuthenticationResponse::PASS, AuthenticationResponse::FAIL ] ) ) {
252            return;
253        }
254        $accountType = $this->identityUtils->getShortUserTypeInternal( $performer );
255
256        $module = $this->module->getModuleName();
257        LoggerFactory::getInstance( 'authevents' )->info( "$module API attempt", [
258            'event' => $event,
259            'successful' => $result->status === AuthenticationResponse::PASS,
260            'status' => $result->message ? $result->message->getKey() : '-',
261            'accountType' => $accountType,
262            'module' => $module,
263        ] );
264    }
265
266    /**
267     * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
268     * @return CreateFromLoginAuthenticationRequest|null
269     */
270    public function getPreservedRequest() {
271        $ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
272        return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
273    }
274
275    /**
276     * Format an array of AuthenticationRequests for return
277     * @param AuthenticationRequest[] $reqs
278     * @return array Will have a 'requests' key, and also 'fields' if $module's
279     *  params include 'mergerequestfields'.
280     */
281    public function formatRequests( array $reqs ) {
282        $params = $this->module->extractRequestParams();
283        $mergeFields = !empty( $params['mergerequestfields'] );
284
285        $ret = [ 'requests' => [] ];
286        foreach ( $reqs as $req ) {
287            $describe = $req->describeCredentials();
288            $reqInfo = [
289                'id' => $req->getUniqueId(),
290                'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ],
291            ];
292            switch ( $req->required ) {
293                case AuthenticationRequest::OPTIONAL:
294                    $reqInfo['required'] = 'optional';
295                    break;
296                case AuthenticationRequest::REQUIRED:
297                    $reqInfo['required'] = 'required';
298                    break;
299                case AuthenticationRequest::PRIMARY_REQUIRED:
300                    $reqInfo['required'] = 'primary-required';
301                    break;
302            }
303            $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
304            $this->formatMessage( $reqInfo, 'account', $describe['account'] );
305            if ( !$mergeFields ) {
306                $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
307            }
308            $ret['requests'][] = $reqInfo;
309        }
310
311        if ( $mergeFields ) {
312            $fields = AuthenticationRequest::mergeFieldInfo( $reqs );
313            $ret['fields'] = $this->formatFields( $fields );
314        }
315
316        return $ret;
317    }
318
319    /**
320     * Clean up a field array for output
321     * @param array $fields
322     * @phpcs:ignore Generic.Files.LineLength
323     * @phan-param array{type:string,options:array,value:string,label:Message,help:Message,optional:bool,sensitive:bool,skippable:bool} $fields
324     * @return array
325     */
326    private function formatFields( array $fields ) {
327        static $copy = [
328            'type' => true,
329            'value' => true,
330        ];
331
332        $module = $this->module;
333        $retFields = [];
334
335        foreach ( $fields as $name => $field ) {
336            $ret = array_intersect_key( $field, $copy );
337
338            if ( isset( $field['options'] ) ) {
339                $ret['options'] = array_map( static function ( $msg ) use ( $module ) {
340                    return $msg->setContext( $module )->plain();
341                }, $field['options'] );
342                ApiResult::setArrayType( $ret['options'], 'assoc' );
343            }
344            $this->formatMessage( $ret, 'label', $field['label'] );
345            $this->formatMessage( $ret, 'help', $field['help'] );
346            $ret['optional'] = !empty( $field['optional'] );
347            $ret['sensitive'] = !empty( $field['sensitive'] );
348
349            $retFields[$name] = $ret;
350        }
351
352        ApiResult::setArrayType( $retFields, 'assoc' );
353
354        return $retFields;
355    }
356
357    /**
358     * Fetch the standard parameters this helper recognizes
359     * @param string $action AuthManager action
360     * @param string ...$wantedParams Parameters to use
361     * @return array
362     */
363    public static function getStandardParams( $action, ...$wantedParams ) {
364        $params = [
365            'requests' => [
366                ParamValidator::PARAM_TYPE => 'string',
367                ParamValidator::PARAM_ISMULTI => true,
368                ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
369            ],
370            'request' => [
371                ParamValidator::PARAM_TYPE => 'string',
372                ParamValidator::PARAM_REQUIRED => true,
373                ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
374            ],
375            'messageformat' => [
376                ParamValidator::PARAM_DEFAULT => 'wikitext',
377                ParamValidator::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
378                ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
379            ],
380            'mergerequestfields' => [
381                ParamValidator::PARAM_DEFAULT => false,
382                ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
383            ],
384            'preservestate' => [
385                ParamValidator::PARAM_DEFAULT => false,
386                ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
387            ],
388            'returnurl' => [
389                ParamValidator::PARAM_TYPE => 'string',
390                ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
391            ],
392            'continue' => [
393                ParamValidator::PARAM_DEFAULT => false,
394                ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
395            ],
396        ];
397
398        $ret = [];
399        foreach ( $wantedParams as $name ) {
400            if ( isset( $params[$name] ) ) {
401                $ret[$name] = $params[$name];
402            }
403        }
404        return $ret;
405    }
406}
407
408/** @deprecated class alias since 1.43 */
409class_alias( ApiAuthManagerHelper::class, 'ApiAuthManagerHelper' );