]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/api/ApiAuthManagerHelper.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / api / ApiAuthManagerHelper.php
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\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                                 ApiResult::setIndexedTagName( $res[$key]['params'], 'param' );
89                                 break;
90                 }
91         }
92
93         /**
94          * Call $manager->securitySensitiveOperationStatus()
95          * @param string $operation Operation being checked.
96          * @throws ApiUsageException
97          */
98         public function securitySensitiveOperation( $operation ) {
99                 $status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
100                 switch ( $status ) {
101                         case AuthManager::SEC_OK:
102                                 return;
103
104                         case AuthManager::SEC_REAUTH:
105                                 $this->module->dieWithError( 'apierror-reauthenticate' );
106
107                         case AuthManager::SEC_FAIL:
108                                 $this->module->dieWithError( 'apierror-cannotreauthenticate' );
109
110                         default:
111                                 throw new UnexpectedValueException( "Unknown status \"$status\"" );
112                 }
113         }
114
115         /**
116          * Filter out authentication requests by class name
117          * @param AuthenticationRequest[] $reqs Requests to filter
118          * @param string[] $blacklist Class names to remove
119          * @return AuthenticationRequest[]
120          */
121         public static function blacklistAuthenticationRequests( array $reqs, array $blacklist ) {
122                 if ( $blacklist ) {
123                         $blacklist = array_flip( $blacklist );
124                         $reqs = array_filter( $reqs, function ( $req ) use ( $blacklist ) {
125                                 return !isset( $blacklist[get_class( $req )] );
126                         } );
127                 }
128                 return $reqs;
129         }
130
131         /**
132          * Fetch and load the AuthenticationRequests for an action
133          * @param string $action One of the AuthManager::ACTION_* constants
134          * @return AuthenticationRequest[]
135          */
136         public function loadAuthenticationRequests( $action ) {
137                 $params = $this->module->extractRequestParams();
138
139                 $manager = AuthManager::singleton();
140                 $reqs = $manager->getAuthenticationRequests( $action, $this->module->getUser() );
141
142                 // Filter requests, if requested to do so
143                 $wantedRequests = null;
144                 if ( isset( $params['requests'] ) ) {
145                         $wantedRequests = array_flip( $params['requests'] );
146                 } elseif ( isset( $params['request'] ) ) {
147                         $wantedRequests = [ $params['request'] => true ];
148                 }
149                 if ( $wantedRequests !== null ) {
150                         $reqs = array_filter( $reqs, function ( $req ) use ( $wantedRequests ) {
151                                 return isset( $wantedRequests[$req->getUniqueId()] );
152                         } );
153                 }
154
155                 // Collect the fields for all the requests
156                 $fields = [];
157                 $sensitive = [];
158                 foreach ( $reqs as $req ) {
159                         $info = (array)$req->getFieldInfo();
160                         $fields += $info;
161                         $sensitive += array_filter( $info, function ( $opts ) {
162                                 return !empty( $opts['sensitive'] );
163                         } );
164                 }
165
166                 // Extract the request data for the fields and mark those request
167                 // parameters as used
168                 $data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
169                 $this->module->getMain()->markParamsUsed( array_keys( $data ) );
170
171                 if ( $sensitive ) {
172                         $this->module->getMain()->markParamsSensitive( array_keys( $sensitive ) );
173                         $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
174                 }
175
176                 return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
177         }
178
179         /**
180          * Format an AuthenticationResponse for return
181          * @param AuthenticationResponse $res
182          * @return array
183          */
184         public function formatAuthenticationResponse( AuthenticationResponse $res ) {
185                 $ret = [
186                         'status' => $res->status,
187                 ];
188
189                 if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
190                         $ret['username'] = $res->username;
191                 }
192
193                 if ( $res->status === AuthenticationResponse::REDIRECT ) {
194                         $ret['redirecttarget'] = $res->redirectTarget;
195                         if ( $res->redirectApiData !== null ) {
196                                 $ret['redirectdata'] = $res->redirectApiData;
197                         }
198                 }
199
200                 if ( $res->status === AuthenticationResponse::REDIRECT ||
201                         $res->status === AuthenticationResponse::UI ||
202                         $res->status === AuthenticationResponse::RESTART
203                 ) {
204                         $ret += $this->formatRequests( $res->neededRequests );
205                 }
206
207                 if ( $res->status === AuthenticationResponse::FAIL ||
208                         $res->status === AuthenticationResponse::UI ||
209                         $res->status === AuthenticationResponse::RESTART
210                 ) {
211                         $this->formatMessage( $ret, 'message', $res->message );
212                         $ret['messagecode'] = ApiMessage::create( $res->message )->getApiCode();
213                 }
214
215                 if ( $res->status === AuthenticationResponse::FAIL ||
216                         $res->status === AuthenticationResponse::RESTART
217                 ) {
218                         $this->module->getRequest()->getSession()->set(
219                                 'ApiAuthManagerHelper::createRequest',
220                                 $res->createRequest
221                         );
222                         $ret['canpreservestate'] = $res->createRequest !== null;
223                 } else {
224                         $this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
225                 }
226
227                 return $ret;
228         }
229
230         /**
231          * Logs successful or failed authentication.
232          * @param string $event Event type (e.g. 'accountcreation')
233          * @param string|AuthenticationResponse $result Response or error message
234          */
235         public function logAuthenticationResult( $event, $result ) {
236                 if ( is_string( $result ) ) {
237                         $status = Status::newFatal( $result );
238                 } elseif ( $result->status === AuthenticationResponse::PASS ) {
239                         $status = Status::newGood();
240                 } elseif ( $result->status === AuthenticationResponse::FAIL ) {
241                         $status = Status::newFatal( $result->message );
242                 } else {
243                         return;
244                 }
245
246                 $module = $this->module->getModuleName();
247                 LoggerFactory::getInstance( 'authevents' )->info( "$module API attempt", [
248                         'event' => $event,
249                         'status' => $status,
250                         'module' => $module,
251                 ] );
252         }
253
254         /**
255          * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
256          * @return CreateFromLoginAuthenticationRequest|null
257          */
258         public function getPreservedRequest() {
259                 $ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
260                 return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
261         }
262
263         /**
264          * Format an array of AuthenticationRequests for return
265          * @param AuthenticationRequest[] $reqs
266          * @return array Will have a 'requests' key, and also 'fields' if $module's
267          *  params include 'mergerequestfields'.
268          */
269         public function formatRequests( array $reqs ) {
270                 $params = $this->module->extractRequestParams();
271                 $mergeFields = !empty( $params['mergerequestfields'] );
272
273                 $ret = [ 'requests' => [] ];
274                 foreach ( $reqs as $req ) {
275                         $describe = $req->describeCredentials();
276                         $reqInfo = [
277                                 'id' => $req->getUniqueId(),
278                                 'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ],
279                         ];
280                         switch ( $req->required ) {
281                                 case AuthenticationRequest::OPTIONAL:
282                                         $reqInfo['required'] = 'optional';
283                                         break;
284                                 case AuthenticationRequest::REQUIRED:
285                                         $reqInfo['required'] = 'required';
286                                         break;
287                                 case AuthenticationRequest::PRIMARY_REQUIRED:
288                                         $reqInfo['required'] = 'primary-required';
289                                         break;
290                         }
291                         $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
292                         $this->formatMessage( $reqInfo, 'account', $describe['account'] );
293                         if ( !$mergeFields ) {
294                                 $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
295                         }
296                         $ret['requests'][] = $reqInfo;
297                 }
298
299                 if ( $mergeFields ) {
300                         $fields = AuthenticationRequest::mergeFieldInfo( $reqs );
301                         $ret['fields'] = $this->formatFields( $fields );
302                 }
303
304                 return $ret;
305         }
306
307         /**
308          * Clean up a field array for output
309          * @param ApiBase $module For context and parameters 'mergerequestfields'
310          *  and 'messageformat'
311          * @param array $fields
312          * @return array
313          */
314         private function formatFields( array $fields ) {
315                 static $copy = [
316                         'type' => true,
317                         'value' => true,
318                 ];
319
320                 $module = $this->module;
321                 $retFields = [];
322
323                 foreach ( $fields as $name => $field ) {
324                         $ret = array_intersect_key( $field, $copy );
325
326                         if ( isset( $field['options'] ) ) {
327                                 $ret['options'] = array_map( function ( $msg ) use ( $module ) {
328                                         return $msg->setContext( $module )->plain();
329                                 }, $field['options'] );
330                                 ApiResult::setArrayType( $ret['options'], 'assoc' );
331                         }
332                         $this->formatMessage( $ret, 'label', $field['label'] );
333                         $this->formatMessage( $ret, 'help', $field['help'] );
334                         $ret['optional'] = !empty( $field['optional'] );
335                         $ret['sensitive'] = !empty( $field['sensitive'] );
336
337                         $retFields[$name] = $ret;
338                 }
339
340                 ApiResult::setArrayType( $retFields, 'assoc' );
341
342                 return $retFields;
343         }
344
345         /**
346          * Fetch the standard parameters this helper recognizes
347          * @param string $action AuthManager action
348          * @param string $param,... Parameters to use
349          * @return array
350          */
351         public static function getStandardParams( $action, $param /* ... */ ) {
352                 $params = [
353                         'requests' => [
354                                 ApiBase::PARAM_TYPE => 'string',
355                                 ApiBase::PARAM_ISMULTI => true,
356                                 ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
357                         ],
358                         'request' => [
359                                 ApiBase::PARAM_TYPE => 'string',
360                                 ApiBase::PARAM_REQUIRED => true,
361                                 ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
362                         ],
363                         'messageformat' => [
364                                 ApiBase::PARAM_DFLT => 'wikitext',
365                                 ApiBase::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
366                                 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
367                         ],
368                         'mergerequestfields' => [
369                                 ApiBase::PARAM_DFLT => false,
370                                 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
371                         ],
372                         'preservestate' => [
373                                 ApiBase::PARAM_DFLT => false,
374                                 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
375                         ],
376                         'returnurl' => [
377                                 ApiBase::PARAM_TYPE => 'string',
378                                 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
379                         ],
380                         'continue' => [
381                                 ApiBase::PARAM_DFLT => false,
382                                 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
383                         ],
384                 ];
385
386                 $ret = [];
387                 $wantedParams = func_get_args();
388                 array_shift( $wantedParams );
389                 foreach ( $wantedParams as $name ) {
390                         if ( isset( $params[$name] ) ) {
391                                 $ret[$name] = $params[$name];
392                         }
393                 }
394                 return $ret;
395         }
396 }