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