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