Merge "Sanitize TestUser"
[lhc/web/wiklou.git] / includes / api / ApiMain.php
1 <?php
2 /**
3 *
4 *
5 * Created on Sep 4, 2006
6 *
7 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
23 *
24 * @file
25 * @defgroup API API
26 */
27
28 /**
29 * This is the main API class, used for both external and internal processing.
30 * When executed, it will create the requested formatter object,
31 * instantiate and execute an object associated with the needed action,
32 * and use formatter to print results.
33 * In case of an exception, an error message will be printed using the same formatter.
34 *
35 * To use API from another application, run it using FauxRequest object, in which
36 * case any internal exceptions will not be handled but passed up to the caller.
37 * After successful execution, use getResult() for the resulting data.
38 *
39 * @ingroup API
40 */
41 class ApiMain extends ApiBase {
42 /**
43 * When no format parameter is given, this format will be used
44 */
45 const API_DEFAULT_FORMAT = 'jsonfm';
46
47 /**
48 * List of available modules: action name => module class
49 */
50 private static $Modules = array(
51 'login' => 'ApiLogin',
52 'logout' => 'ApiLogout',
53 'createaccount' => 'ApiCreateAccount',
54 'query' => 'ApiQuery',
55 'expandtemplates' => 'ApiExpandTemplates',
56 'parse' => 'ApiParse',
57 'opensearch' => 'ApiOpenSearch',
58 'feedcontributions' => 'ApiFeedContributions',
59 'feedrecentchanges' => 'ApiFeedRecentChanges',
60 'feedwatchlist' => 'ApiFeedWatchlist',
61 'help' => 'ApiHelp',
62 'paraminfo' => 'ApiParamInfo',
63 'rsd' => 'ApiRsd',
64 'compare' => 'ApiComparePages',
65 'tokens' => 'ApiTokens',
66
67 // Write modules
68 'purge' => 'ApiPurge',
69 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
70 'rollback' => 'ApiRollback',
71 'delete' => 'ApiDelete',
72 'undelete' => 'ApiUndelete',
73 'protect' => 'ApiProtect',
74 'block' => 'ApiBlock',
75 'unblock' => 'ApiUnblock',
76 'move' => 'ApiMove',
77 'edit' => 'ApiEditPage',
78 'upload' => 'ApiUpload',
79 'filerevert' => 'ApiFileRevert',
80 'emailuser' => 'ApiEmailUser',
81 'watch' => 'ApiWatch',
82 'patrol' => 'ApiPatrol',
83 'import' => 'ApiImport',
84 'clearhasmsg' => 'ApiClearHasMsg',
85 'userrights' => 'ApiUserrights',
86 'options' => 'ApiOptions',
87 'imagerotate' => 'ApiImageRotate',
88 'revisiondelete' => 'ApiRevisionDelete',
89 );
90
91 /**
92 * List of available formats: format name => format class
93 */
94 private static $Formats = array(
95 'json' => 'ApiFormatJson',
96 'jsonfm' => 'ApiFormatJson',
97 'php' => 'ApiFormatPhp',
98 'phpfm' => 'ApiFormatPhp',
99 'wddx' => 'ApiFormatWddx',
100 'wddxfm' => 'ApiFormatWddx',
101 'xml' => 'ApiFormatXml',
102 'xmlfm' => 'ApiFormatXml',
103 'yaml' => 'ApiFormatYaml',
104 'yamlfm' => 'ApiFormatYaml',
105 'rawfm' => 'ApiFormatJson',
106 'txt' => 'ApiFormatTxt',
107 'txtfm' => 'ApiFormatTxt',
108 'dbg' => 'ApiFormatDbg',
109 'dbgfm' => 'ApiFormatDbg',
110 'dump' => 'ApiFormatDump',
111 'dumpfm' => 'ApiFormatDump',
112 'none' => 'ApiFormatNone',
113 );
114
115 // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
116 /**
117 * List of user roles that are specifically relevant to the API.
118 * array( 'right' => array ( 'msg' => 'Some message with a $1',
119 * 'params' => array ( $someVarToSubst ) ),
120 * );
121 */
122 private static $mRights = array(
123 'writeapi' => array(
124 'msg' => 'right-writeapi',
125 'params' => array()
126 ),
127 'apihighlimits' => array(
128 'msg' => 'api-help-right-apihighlimits',
129 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 )
130 )
131 );
132 // @codingStandardsIgnoreEnd
133
134 /**
135 * @var ApiFormatBase
136 */
137 private $mPrinter;
138
139 private $mModuleMgr, $mResult;
140 private $mAction;
141 private $mEnableWrite;
142 private $mInternalMode, $mSquidMaxage, $mModule;
143
144 private $mCacheMode = 'private';
145 private $mCacheControl = array();
146 private $mParamsUsed = array();
147
148 /**
149 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
150 *
151 * @param IContextSource|WebRequest $context If this is an instance of
152 * FauxRequest, errors are thrown and no printing occurs
153 * @param bool $enableWrite Should be set to true if the api may modify data
154 */
155 public function __construct( $context = null, $enableWrite = false ) {
156 if ( $context === null ) {
157 $context = RequestContext::getMain();
158 } elseif ( $context instanceof WebRequest ) {
159 // BC for pre-1.19
160 $request = $context;
161 $context = RequestContext::getMain();
162 }
163 // We set a derivative context so we can change stuff later
164 $this->setContext( new DerivativeContext( $context ) );
165
166 if ( isset( $request ) ) {
167 $this->getContext()->setRequest( $request );
168 }
169
170 $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
171
172 // Special handling for the main module: $parent === $this
173 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
174
175 if ( !$this->mInternalMode ) {
176 // Impose module restrictions.
177 // If the current user cannot read,
178 // Remove all modules other than login
179 global $wgUser;
180
181 if ( $this->getVal( 'callback' ) !== null ) {
182 // JSON callback allows cross-site reads.
183 // For safety, strip user credentials.
184 wfDebug( "API: stripping user credentials for JSON callback\n" );
185 $wgUser = new User();
186 $this->getContext()->setUser( $wgUser );
187 }
188 }
189
190 $uselang = $this->getParameter( 'uselang' );
191 if ( $uselang === 'user' ) {
192 $uselang = $this->getUser()->getOption( 'language' );
193 $uselang = RequestContext::sanitizeLangCode( $uselang );
194 wfRunHooks( 'UserGetLanguageObject', array( $this->getUser(), &$uselang, $this ) );
195 }
196 $code = RequestContext::sanitizeLangCode( $uselang );
197 $this->getContext()->setLanguage( $code );
198 if ( !$this->mInternalMode ) {
199 global $wgLang;
200 $wgLang = RequestContext::getMain()->getLanguage();
201 }
202
203 $config = $this->getConfig();
204 $this->mModuleMgr = new ApiModuleManager( $this );
205 $this->mModuleMgr->addModules( self::$Modules, 'action' );
206 $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
207 $this->mModuleMgr->addModules( self::$Formats, 'format' );
208 $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
209
210 $this->mResult = new ApiResult( $this );
211 $this->mEnableWrite = $enableWrite;
212
213 $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
214 $this->mCommit = false;
215 }
216
217 /**
218 * Return true if the API was started by other PHP code using FauxRequest
219 * @return bool
220 */
221 public function isInternalMode() {
222 return $this->mInternalMode;
223 }
224
225 /**
226 * Get the ApiResult object associated with current request
227 *
228 * @return ApiResult
229 */
230 public function getResult() {
231 return $this->mResult;
232 }
233
234 /**
235 * Get the API module object. Only works after executeAction()
236 *
237 * @return ApiBase
238 */
239 public function getModule() {
240 return $this->mModule;
241 }
242
243 /**
244 * Get the result formatter object. Only works after setupExecuteAction()
245 *
246 * @return ApiFormatBase
247 */
248 public function getPrinter() {
249 return $this->mPrinter;
250 }
251
252 /**
253 * Set how long the response should be cached.
254 *
255 * @param int $maxage
256 */
257 public function setCacheMaxAge( $maxage ) {
258 $this->setCacheControl( array(
259 'max-age' => $maxage,
260 's-maxage' => $maxage
261 ) );
262 }
263
264 /**
265 * Set the type of caching headers which will be sent.
266 *
267 * @param string $mode One of:
268 * - 'public': Cache this object in public caches, if the maxage or smaxage
269 * parameter is set, or if setCacheMaxAge() was called. If a maximum age is
270 * not provided by any of these means, the object will be private.
271 * - 'private': Cache this object only in private client-side caches.
272 * - 'anon-public-user-private': Make this object cacheable for logged-out
273 * users, but private for logged-in users. IMPORTANT: If this is set, it must be
274 * set consistently for a given URL, it cannot be set differently depending on
275 * things like the contents of the database, or whether the user is logged in.
276 *
277 * If the wiki does not allow anonymous users to read it, the mode set here
278 * will be ignored, and private caching headers will always be sent. In other words,
279 * the "public" mode is equivalent to saying that the data sent is as public as a page
280 * view.
281 *
282 * For user-dependent data, the private mode should generally be used. The
283 * anon-public-user-private mode should only be used where there is a particularly
284 * good performance reason for caching the anonymous response, but where the
285 * response to logged-in users may differ, or may contain private data.
286 *
287 * If this function is never called, then the default will be the private mode.
288 */
289 public function setCacheMode( $mode ) {
290 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) {
291 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
292
293 // Ignore for forwards-compatibility
294 return;
295 }
296
297 if ( !User::isEveryoneAllowed( 'read' ) ) {
298 // Private wiki, only private headers
299 if ( $mode !== 'private' ) {
300 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
301
302 return;
303 }
304 }
305
306 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
307 // User language is used for i18n, so we don't want to publicly
308 // cache. Anons are ok, because if they have non-default language
309 // then there's an appropriate Vary header set by whatever set
310 // their non-default language.
311 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
312 "'anon-public-user-private' due to uselang=user\n" );
313 $mode = 'anon-public-user-private';
314 }
315
316 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
317 $this->mCacheMode = $mode;
318 }
319
320 /**
321 * Set directives (key/value pairs) for the Cache-Control header.
322 * Boolean values will be formatted as such, by including or omitting
323 * without an equals sign.
324 *
325 * Cache control values set here will only be used if the cache mode is not
326 * private, see setCacheMode().
327 *
328 * @param array $directives
329 */
330 public function setCacheControl( $directives ) {
331 $this->mCacheControl = $directives + $this->mCacheControl;
332 }
333
334 /**
335 * Create an instance of an output formatter by its name
336 *
337 * @param string $format
338 *
339 * @return ApiFormatBase
340 */
341 public function createPrinterByName( $format ) {
342 $printer = $this->mModuleMgr->getModule( $format, 'format' );
343 if ( $printer === null ) {
344 $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
345 }
346
347 return $printer;
348 }
349
350 /**
351 * Execute api request. Any errors will be handled if the API was called by the remote client.
352 */
353 public function execute() {
354 $this->profileIn();
355 if ( $this->mInternalMode ) {
356 $this->executeAction();
357 } else {
358 $this->executeActionWithErrorHandling();
359 }
360
361 $this->profileOut();
362 }
363
364 /**
365 * Execute an action, and in case of an error, erase whatever partial results
366 * have been accumulated, and replace it with an error message and a help screen.
367 */
368 protected function executeActionWithErrorHandling() {
369 // Verify the CORS header before executing the action
370 if ( !$this->handleCORS() ) {
371 // handleCORS() has sent a 403, abort
372 return;
373 }
374
375 // Exit here if the request method was OPTIONS
376 // (assume there will be a followup GET or POST)
377 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
378 return;
379 }
380
381 // In case an error occurs during data output,
382 // clear the output buffer and print just the error information
383 ob_start();
384
385 $t = microtime( true );
386 try {
387 $this->executeAction();
388 } catch ( Exception $e ) {
389 $this->handleException( $e );
390 }
391
392 // Log the request whether or not there was an error
393 $this->logRequest( microtime( true ) - $t );
394
395 // Send cache headers after any code which might generate an error, to
396 // avoid sending public cache headers for errors.
397 $this->sendCacheHeaders();
398
399 ob_end_flush();
400 }
401
402 /**
403 * Handle an exception as an API response
404 *
405 * @since 1.23
406 * @param Exception $e
407 */
408 protected function handleException( Exception $e ) {
409 // Bug 63145: Rollback any open database transactions
410 if ( !( $e instanceof UsageException ) ) {
411 // UsageExceptions are intentional, so don't rollback if that's the case
412 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
413 }
414
415 // Allow extra cleanup and logging
416 wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
417
418 // Log it
419 if ( !( $e instanceof UsageException ) ) {
420 MWExceptionHandler::logException( $e );
421 }
422
423 // Handle any kind of exception by outputting properly formatted error message.
424 // If this fails, an unhandled exception should be thrown so that global error
425 // handler will process and log it.
426
427 $errCode = $this->substituteResultWithError( $e );
428
429 // Error results should not be cached
430 $this->setCacheMode( 'private' );
431
432 $response = $this->getRequest()->response();
433 $headerStr = 'MediaWiki-API-Error: ' . $errCode;
434 if ( $e->getCode() === 0 ) {
435 $response->header( $headerStr );
436 } else {
437 $response->header( $headerStr, true, $e->getCode() );
438 }
439
440 // Reset and print just the error message
441 ob_clean();
442
443 // If the error occurred during printing, do a printer->profileOut()
444 $this->mPrinter->safeProfileOut();
445 $this->printResult( true );
446 }
447
448 /**
449 * Handle an exception from the ApiBeforeMain hook.
450 *
451 * This tries to print the exception as an API response, to be more
452 * friendly to clients. If it fails, it will rethrow the exception.
453 *
454 * @since 1.23
455 * @param Exception $e
456 */
457 public static function handleApiBeforeMainException( Exception $e ) {
458 ob_start();
459
460 try {
461 $main = new self( RequestContext::getMain(), false );
462 $main->handleException( $e );
463 } catch ( Exception $e2 ) {
464 // Nope, even that didn't work. Punt.
465 throw $e;
466 }
467
468 // Log the request and reset cache headers
469 $main->logRequest( 0 );
470 $main->sendCacheHeaders();
471
472 ob_end_flush();
473 }
474
475 /**
476 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
477 *
478 * If no origin parameter is present, nothing happens.
479 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
480 * is set and false is returned.
481 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
482 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
483 * headers are set.
484 *
485 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
486 */
487 protected function handleCORS() {
488 $originParam = $this->getParameter( 'origin' ); // defaults to null
489 if ( $originParam === null ) {
490 // No origin parameter, nothing to do
491 return true;
492 }
493
494 $request = $this->getRequest();
495 $response = $request->response();
496 // Origin: header is a space-separated list of origins, check all of them
497 $originHeader = $request->getHeader( 'Origin' );
498 if ( $originHeader === false ) {
499 $origins = array();
500 } else {
501 $origins = explode( ' ', $originHeader );
502 }
503
504 if ( !in_array( $originParam, $origins ) ) {
505 // origin parameter set but incorrect
506 // Send a 403 response
507 $message = HttpStatus::getMessage( 403 );
508 $response->header( "HTTP/1.1 403 $message", true, 403 );
509 $response->header( 'Cache-Control: no-cache' );
510 echo "'origin' parameter does not match Origin header\n";
511
512 return false;
513 }
514
515 $config = $this->getConfig();
516 $matchOrigin = self::matchOrigin(
517 $originParam,
518 $config->get( 'CrossSiteAJAXdomains' ),
519 $config->get( 'CrossSiteAJAXdomainExceptions' )
520 );
521
522 if ( $matchOrigin ) {
523 $response->header( "Access-Control-Allow-Origin: $originParam" );
524 $response->header( 'Access-Control-Allow-Credentials: true' );
525 $this->getOutput()->addVaryHeader( 'Origin' );
526 }
527
528 return true;
529 }
530
531 /**
532 * Attempt to match an Origin header against a set of rules and a set of exceptions
533 * @param string $value Origin header
534 * @param array $rules Set of wildcard rules
535 * @param array $exceptions Set of wildcard rules
536 * @return bool True if $value matches a rule in $rules and doesn't match
537 * any rules in $exceptions, false otherwise
538 */
539 protected static function matchOrigin( $value, $rules, $exceptions ) {
540 foreach ( $rules as $rule ) {
541 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
542 // Rule matches, check exceptions
543 foreach ( $exceptions as $exc ) {
544 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
545 return false;
546 }
547 }
548
549 return true;
550 }
551 }
552
553 return false;
554 }
555
556 /**
557 * Helper function to convert wildcard string into a regex
558 * '*' => '.*?'
559 * '?' => '.'
560 *
561 * @param string $wildcard String with wildcards
562 * @return string Regular expression
563 */
564 protected static function wildcardToRegex( $wildcard ) {
565 $wildcard = preg_quote( $wildcard, '/' );
566 $wildcard = str_replace(
567 array( '\*', '\?' ),
568 array( '.*?', '.' ),
569 $wildcard
570 );
571
572 return "/https?:\/\/$wildcard/";
573 }
574
575 protected function sendCacheHeaders() {
576 $response = $this->getRequest()->response();
577 $out = $this->getOutput();
578
579 $config = $this->getConfig();
580
581 if ( $config->get( 'VaryOnXFP' ) ) {
582 $out->addVaryHeader( 'X-Forwarded-Proto' );
583 }
584
585 if ( $this->mCacheMode == 'private' ) {
586 $response->header( 'Cache-Control: private' );
587 return;
588 }
589
590 $useXVO = $config->get( 'UseXVO' );
591 if ( $this->mCacheMode == 'anon-public-user-private' ) {
592 $out->addVaryHeader( 'Cookie' );
593 $response->header( $out->getVaryHeader() );
594 if ( $useXVO ) {
595 $response->header( $out->getXVO() );
596 if ( $out->haveCacheVaryCookies() ) {
597 // Logged in, mark this request private
598 $response->header( 'Cache-Control: private' );
599 return;
600 }
601 // Logged out, send normal public headers below
602 } elseif ( session_id() != '' ) {
603 // Logged in or otherwise has session (e.g. anonymous users who have edited)
604 // Mark request private
605 $response->header( 'Cache-Control: private' );
606
607 return;
608 } // else no XVO and anonymous, send public headers below
609 }
610
611 // Send public headers
612 $response->header( $out->getVaryHeader() );
613 if ( $useXVO ) {
614 $response->header( $out->getXVO() );
615 }
616
617 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
618 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
619 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
620 }
621 if ( !isset( $this->mCacheControl['max-age'] ) ) {
622 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
623 }
624
625 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
626 // Public cache not requested
627 // Sending a Vary header in this case is harmless, and protects us
628 // against conditional calls of setCacheMaxAge().
629 $response->header( 'Cache-Control: private' );
630
631 return;
632 }
633
634 $this->mCacheControl['public'] = true;
635
636 // Send an Expires header
637 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
638 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
639 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
640
641 // Construct the Cache-Control header
642 $ccHeader = '';
643 $separator = '';
644 foreach ( $this->mCacheControl as $name => $value ) {
645 if ( is_bool( $value ) ) {
646 if ( $value ) {
647 $ccHeader .= $separator . $name;
648 $separator = ', ';
649 }
650 } else {
651 $ccHeader .= $separator . "$name=$value";
652 $separator = ', ';
653 }
654 }
655
656 $response->header( "Cache-Control: $ccHeader" );
657 }
658
659 /**
660 * Replace the result data with the information about an exception.
661 * Returns the error code
662 * @param Exception $e
663 * @return string
664 */
665 protected function substituteResultWithError( $e ) {
666 $result = $this->getResult();
667
668 // Printer may not be initialized if the extractRequestParams() fails for the main module
669 if ( !isset( $this->mPrinter ) ) {
670 // The printer has not been created yet. Try to manually get formatter value.
671 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
672 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
673 $value = self::API_DEFAULT_FORMAT;
674 }
675
676 $this->mPrinter = $this->createPrinterByName( $value );
677 }
678
679 // Printer may not be able to handle errors. This is particularly
680 // likely if the module returns something for getCustomPrinter().
681 if ( !$this->mPrinter->canPrintErrors() ) {
682 $this->mPrinter->safeProfileOut();
683 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
684 }
685
686 // Update raw mode flag for the selected printer.
687 $result->setRawMode( $this->mPrinter->getNeedsRawData() );
688
689 $config = $this->getConfig();
690
691 if ( $e instanceof UsageException ) {
692 // User entered incorrect parameters - generate error response
693 $errMessage = $e->getMessageArray();
694 $link = wfExpandUrl( wfScript( 'api' ) );
695 ApiResult::setContent( $errMessage, "See $link for API usage" );
696 } else {
697 // Something is seriously wrong
698 if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
699 $info = 'Database query error';
700 } else {
701 $info = "Exception Caught: {$e->getMessage()}";
702 }
703
704 $errMessage = array(
705 'code' => 'internal_api_error_' . get_class( $e ),
706 'info' => $info,
707 );
708 ApiResult::setContent(
709 $errMessage,
710 $config->get( 'ShowExceptionDetails' ) ? "\n\n{$e->getTraceAsString()}\n\n" : ''
711 );
712 }
713
714 // Remember all the warnings to re-add them later
715 $oldResult = $result->getData();
716 $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
717
718 $result->reset();
719 // Re-add the id
720 $requestid = $this->getParameter( 'requestid' );
721 if ( !is_null( $requestid ) ) {
722 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
723 }
724 if ( $config->get( 'ShowHostnames' ) ) {
725 // servedby is especially useful when debugging errors
726 $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK );
727 }
728 if ( $warnings !== null ) {
729 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
730 }
731
732 $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
733
734 return $errMessage['code'];
735 }
736
737 /**
738 * Set up for the execution.
739 * @return array
740 */
741 protected function setupExecuteAction() {
742 // First add the id to the top element
743 $result = $this->getResult();
744 $requestid = $this->getParameter( 'requestid' );
745 if ( !is_null( $requestid ) ) {
746 $result->addValue( null, 'requestid', $requestid );
747 }
748
749 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
750 $servedby = $this->getParameter( 'servedby' );
751 if ( $servedby ) {
752 $result->addValue( null, 'servedby', wfHostName() );
753 }
754 }
755
756 if ( $this->getParameter( 'curtimestamp' ) ) {
757 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
758 ApiResult::NO_SIZE_CHECK );
759 }
760
761 $params = $this->extractRequestParams();
762
763 $this->mAction = $params['action'];
764
765 if ( !is_string( $this->mAction ) ) {
766 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
767 }
768
769 return $params;
770 }
771
772 /**
773 * Set up the module for response
774 * @return ApiBase The module that will handle this action
775 */
776 protected function setupModule() {
777 // Instantiate the module requested by the user
778 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
779 if ( $module === null ) {
780 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
781 }
782 $moduleParams = $module->extractRequestParams();
783
784 // Check token, if necessary
785 if ( $module->needsToken() === true ) {
786 throw new MWException(
787 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
788 "See documentation for ApiBase::needsToken for details."
789 );
790 }
791 if ( $module->needsToken() ) {
792 if ( !$module->mustBePosted() ) {
793 throw new MWException(
794 "Module '{$module->getModuleName()}' must require POST to use tokens."
795 );
796 }
797
798 if ( !isset( $moduleParams['token'] ) ) {
799 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
800 }
801
802 if ( !$this->getConfig()->get( 'DebugAPI' ) &&
803 array_key_exists(
804 $module->encodeParamName( 'token' ),
805 $this->getRequest()->getQueryValues()
806 )
807 ) {
808 $this->dieUsage(
809 "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body",
810 'mustposttoken'
811 );
812 }
813
814 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
815 $this->dieUsageMsg( 'sessionfailure' );
816 }
817 }
818
819 return $module;
820 }
821
822 /**
823 * Check the max lag if necessary
824 * @param ApiBase $module Api module being used
825 * @param array $params Array an array containing the request parameters.
826 * @return bool True on success, false should exit immediately
827 */
828 protected function checkMaxLag( $module, $params ) {
829 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
830 // Check for maxlag
831 $maxLag = $params['maxlag'];
832 list( $host, $lag ) = wfGetLB()->getMaxLag();
833 if ( $lag > $maxLag ) {
834 $response = $this->getRequest()->response();
835
836 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
837 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
838
839 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
840 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
841 }
842
843 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
844 }
845 }
846
847 return true;
848 }
849
850 /**
851 * Check for sufficient permissions to execute
852 * @param ApiBase $module An Api module
853 */
854 protected function checkExecutePermissions( $module ) {
855 $user = $this->getUser();
856 if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
857 !$user->isAllowed( 'read' )
858 ) {
859 $this->dieUsageMsg( 'readrequired' );
860 }
861 if ( $module->isWriteMode() ) {
862 if ( !$this->mEnableWrite ) {
863 $this->dieUsageMsg( 'writedisabled' );
864 }
865 if ( !$user->isAllowed( 'writeapi' ) ) {
866 $this->dieUsageMsg( 'writerequired' );
867 }
868 if ( wfReadOnly() ) {
869 $this->dieReadOnly();
870 }
871 }
872
873 // Allow extensions to stop execution for arbitrary reasons.
874 $message = false;
875 if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
876 $this->dieUsageMsg( $message );
877 }
878 }
879
880 /**
881 * Check asserts of the user's rights
882 * @param array $params
883 */
884 protected function checkAsserts( $params ) {
885 if ( isset( $params['assert'] ) ) {
886 $user = $this->getUser();
887 switch ( $params['assert'] ) {
888 case 'user':
889 if ( $user->isAnon() ) {
890 $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
891 }
892 break;
893 case 'bot':
894 if ( !$user->isAllowed( 'bot' ) ) {
895 $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
896 }
897 break;
898 }
899 }
900 }
901
902 /**
903 * Check POST for external response and setup result printer
904 * @param ApiBase $module An Api module
905 * @param array $params An array with the request parameters
906 */
907 protected function setupExternalResponse( $module, $params ) {
908 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
909 // Module requires POST. GET request might still be allowed
910 // if $wgDebugApi is true, otherwise fail.
911 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) );
912 }
913
914 // See if custom printer is used
915 $this->mPrinter = $module->getCustomPrinter();
916 if ( is_null( $this->mPrinter ) ) {
917 // Create an appropriate printer
918 $this->mPrinter = $this->createPrinterByName( $params['format'] );
919 }
920
921 if ( $this->mPrinter->getNeedsRawData() ) {
922 $this->getResult()->setRawMode();
923 }
924 }
925
926 /**
927 * Execute the actual module, without any error handling
928 */
929 protected function executeAction() {
930 $params = $this->setupExecuteAction();
931 $module = $this->setupModule();
932 $this->mModule = $module;
933
934 $this->checkExecutePermissions( $module );
935
936 if ( !$this->checkMaxLag( $module, $params ) ) {
937 return;
938 }
939
940 if ( !$this->mInternalMode ) {
941 $this->setupExternalResponse( $module, $params );
942 }
943
944 $this->checkAsserts( $params );
945
946 // Execute
947 $module->profileIn();
948 $module->execute();
949 wfRunHooks( 'APIAfterExecute', array( &$module ) );
950 $module->profileOut();
951
952 $this->reportUnusedParams();
953
954 if ( !$this->mInternalMode ) {
955 //append Debug information
956 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
957
958 // Print result data
959 $this->printResult( false );
960 }
961 }
962
963 /**
964 * Log the preceding request
965 * @param int $time Time in seconds
966 */
967 protected function logRequest( $time ) {
968 $request = $this->getRequest();
969 $milliseconds = $time === null ? '?' : round( $time * 1000 );
970 $s = 'API' .
971 ' ' . $request->getMethod() .
972 ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
973 ' ' . $request->getIP() .
974 ' T=' . $milliseconds . 'ms';
975 foreach ( $this->getParamsUsed() as $name ) {
976 $value = $request->getVal( $name );
977 if ( $value === null ) {
978 continue;
979 }
980 $s .= ' ' . $name . '=';
981 if ( strlen( $value ) > 256 ) {
982 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
983 $s .= $encValue . '[...]';
984 } else {
985 $s .= $this->encodeRequestLogValue( $value );
986 }
987 }
988 $s .= "\n";
989 wfDebugLog( 'api', $s, 'private' );
990 }
991
992 /**
993 * Encode a value in a format suitable for a space-separated log line.
994 * @param string $s
995 * @return string
996 */
997 protected function encodeRequestLogValue( $s ) {
998 static $table;
999 if ( !$table ) {
1000 $chars = ';@$!*(),/:';
1001 $numChars = strlen( $chars );
1002 for ( $i = 0; $i < $numChars; $i++ ) {
1003 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1004 }
1005 }
1006
1007 return strtr( rawurlencode( $s ), $table );
1008 }
1009
1010 /**
1011 * Get the request parameters used in the course of the preceding execute() request
1012 * @return array
1013 */
1014 protected function getParamsUsed() {
1015 return array_keys( $this->mParamsUsed );
1016 }
1017
1018 /**
1019 * Get a request value, and register the fact that it was used, for logging.
1020 * @param string $name
1021 * @param mixed $default
1022 * @return mixed
1023 */
1024 public function getVal( $name, $default = null ) {
1025 $this->mParamsUsed[$name] = true;
1026
1027 $ret = $this->getRequest()->getVal( $name );
1028 if ( $ret === null ) {
1029 if ( $this->getRequest()->getArray( $name ) !== null ) {
1030 // See bug 10262 for why we don't just join( '|', ... ) the
1031 // array.
1032 $this->setWarning(
1033 "Parameter '$name' uses unsupported PHP array syntax"
1034 );
1035 }
1036 $ret = $default;
1037 }
1038 return $ret;
1039 }
1040
1041 /**
1042 * Get a boolean request value, and register the fact that the parameter
1043 * was used, for logging.
1044 * @param string $name
1045 * @return bool
1046 */
1047 public function getCheck( $name ) {
1048 return $this->getVal( $name, null ) !== null;
1049 }
1050
1051 /**
1052 * Get a request upload, and register the fact that it was used, for logging.
1053 *
1054 * @since 1.21
1055 * @param string $name Parameter name
1056 * @return WebRequestUpload
1057 */
1058 public function getUpload( $name ) {
1059 $this->mParamsUsed[$name] = true;
1060
1061 return $this->getRequest()->getUpload( $name );
1062 }
1063
1064 /**
1065 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
1066 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
1067 */
1068 protected function reportUnusedParams() {
1069 $paramsUsed = $this->getParamsUsed();
1070 $allParams = $this->getRequest()->getValueNames();
1071
1072 if ( !$this->mInternalMode ) {
1073 // Printer has not yet executed; don't warn that its parameters are unused
1074 $printerParams = array_map(
1075 array( $this->mPrinter, 'encodeParamName' ),
1076 array_keys( $this->mPrinter->getFinalParams() ?: array() )
1077 );
1078 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1079 } else {
1080 $unusedParams = array_diff( $allParams, $paramsUsed );
1081 }
1082
1083 if ( count( $unusedParams ) ) {
1084 $s = count( $unusedParams ) > 1 ? 's' : '';
1085 $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
1086 }
1087 }
1088
1089 /**
1090 * Print results using the current printer
1091 *
1092 * @param bool $isError
1093 */
1094 protected function printResult( $isError ) {
1095 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1096 $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
1097 }
1098
1099 $this->getResult()->cleanUpUTF8();
1100 $printer = $this->mPrinter;
1101 $printer->profileIn();
1102
1103 $printer->initPrinter( false );
1104
1105 $printer->execute();
1106 $printer->closePrinter();
1107 $printer->profileOut();
1108 }
1109
1110 /**
1111 * @return bool
1112 */
1113 public function isReadMode() {
1114 return false;
1115 }
1116
1117 /**
1118 * See ApiBase for description.
1119 *
1120 * @return array
1121 */
1122 public function getAllowedParams() {
1123 global $wgContLang;
1124
1125 return array(
1126 'action' => array(
1127 ApiBase::PARAM_DFLT => 'help',
1128 ApiBase::PARAM_TYPE => 'submodule',
1129 ),
1130 'format' => array(
1131 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
1132 ApiBase::PARAM_TYPE => 'submodule',
1133 ),
1134 'maxlag' => array(
1135 ApiBase::PARAM_TYPE => 'integer'
1136 ),
1137 'smaxage' => array(
1138 ApiBase::PARAM_TYPE => 'integer',
1139 ApiBase::PARAM_DFLT => 0
1140 ),
1141 'maxage' => array(
1142 ApiBase::PARAM_TYPE => 'integer',
1143 ApiBase::PARAM_DFLT => 0
1144 ),
1145 'assert' => array(
1146 ApiBase::PARAM_TYPE => array( 'user', 'bot' )
1147 ),
1148 'requestid' => null,
1149 'servedby' => false,
1150 'curtimestamp' => false,
1151 'origin' => null,
1152 'uselang' => array(
1153 ApiBase::PARAM_DFLT => $wgContLang->getCode(),
1154 ),
1155 );
1156 }
1157
1158 /** @see ApiBase::getExamplesMessages() */
1159 public function getExamplesMessages() {
1160 return array(
1161 'action=help' => 'apihelp-help-example-main',
1162 'action=help&recursivesubmodules=1' => 'apihelp-help-example-recursive',
1163 );
1164 }
1165
1166 public function modifyHelp( array &$help, array $options ) {
1167 // Wish PHP had an "array_insert_before". Instead, we have to manually
1168 // reindex the array to get 'permissions' in the right place.
1169 $oldHelp = $help;
1170 $help = array();
1171 foreach ( $oldHelp as $k => $v ) {
1172 if ( $k === 'submodules' ) {
1173 $help['permissions'] = '';
1174 }
1175 $help[$k] = $v;
1176 }
1177 $help['credits'] = '';
1178
1179 // Fill 'permissions'
1180 $help['permissions'] .= Html::openElement( 'div',
1181 array( 'class' => 'apihelp-block apihelp-permissions' ) );
1182 $m = $this->msg( 'api-help-permissions' );
1183 if ( !$m->isDisabled() ) {
1184 $help['permissions'] .= Html::rawElement( 'div', array( 'class' => 'apihelp-block-head' ),
1185 $m->numParams( count( self::$mRights ) )->parse()
1186 );
1187 }
1188 $help['permissions'] .= Html::openElement( 'dl' );
1189 foreach ( self::$mRights as $right => $rightMsg ) {
1190 $help['permissions'] .= Html::element( 'dt', null, $right );
1191
1192 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1193 $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1194
1195 $groups = array_map( function ( $group ) {
1196 return $group == '*' ? 'all' : $group;
1197 }, User::getGroupsWithPermission( $right ) );
1198
1199 $help['permissions'] .= Html::rawElement( 'dd', null,
1200 $this->msg( 'api-help-permissions-granted-to' )
1201 ->numParams( count( $groups ) )
1202 ->params( $this->getLanguage()->commaList( $groups ) )
1203 ->parse()
1204 );
1205 }
1206 $help['permissions'] .= Html::closeElement( 'dl' );
1207 $help['permissions'] .= Html::closeElement( 'div' );
1208
1209 // Fill 'credits', if applicable
1210 if ( empty( $options['nolead'] ) ) {
1211 $help['credits'] .= Html::element( 'h' . min( 6, $options['headerlevel'] + 1 ),
1212 array( 'id' => '+credits', 'class' => 'apihelp-header' ),
1213 $this->msg( 'api-credits-header' )->parse()
1214 );
1215 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1216 }
1217 }
1218
1219 private $mCanApiHighLimits = null;
1220
1221 /**
1222 * Check whether the current user is allowed to use high limits
1223 * @return bool
1224 */
1225 public function canApiHighLimits() {
1226 if ( !isset( $this->mCanApiHighLimits ) ) {
1227 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
1228 }
1229
1230 return $this->mCanApiHighLimits;
1231 }
1232
1233 /**
1234 * Overrides to return this instance's module manager.
1235 * @return ApiModuleManager
1236 */
1237 public function getModuleManager() {
1238 return $this->mModuleMgr;
1239 }
1240
1241 /************************************************************************//**
1242 * @name Deprecated
1243 * @{
1244 */
1245
1246 /**
1247 * @deprecated since 1.25
1248 * @return array
1249 */
1250 public function getParamDescription() {
1251 return array(
1252 'format' => 'The format of the output',
1253 'action' => 'What action you would like to perform. See below for module help',
1254 'maxlag' => array(
1255 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
1256 'To save actions causing any more site replication lag, this parameter can make the client',
1257 'wait until the replication lag is less than the specified value.',
1258 'In case of a replag error, error code "maxlag" is returned, with the message like',
1259 '"Waiting for $host: $lag seconds lagged\n".',
1260 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
1261 ),
1262 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
1263 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
1264 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"',
1265 'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
1266 'servedby' => 'Include the hostname that served the request in the ' .
1267 'results. Unconditionally shown on error',
1268 'curtimestamp' => 'Include the current timestamp in the result.',
1269 'origin' => array(
1270 'When accessing the API using a cross-domain AJAX request (CORS), set this to the',
1271 'originating domain. This must be included in any pre-flight request, and',
1272 'therefore must be part of the request URI (not the POST body). This must match',
1273 'one of the origins in the Origin: header exactly, so it has to be set to ',
1274 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this',
1275 'parameter does not match the Origin: header, a 403 response will be returned. If',
1276 'this parameter matches the Origin: header and the origin is whitelisted, an',
1277 'Access-Control-Allow-Origin header will be set.',
1278 ),
1279 );
1280 }
1281
1282 /**
1283 * @deprecated since 1.25
1284 * @return array
1285 */
1286 public function getDescription() {
1287 return array(
1288 '',
1289 '',
1290 '**********************************************************************************************',
1291 '** **',
1292 '** This is an auto-generated MediaWiki API documentation page **',
1293 '** **',
1294 '** Documentation and Examples: **',
1295 '** https://www.mediawiki.org/wiki/API **',
1296 '** **',
1297 '**********************************************************************************************',
1298 '',
1299 'Status: All features shown on this page should be working, but the API',
1300 ' is still in active development, and may change at any time.',
1301 ' Make sure to monitor our mailing list for any updates.',
1302 '',
1303 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent',
1304 ' with the key "MediaWiki-API-Error" and then both the value of the',
1305 ' header and the error code sent back will be set to the same value.',
1306 '',
1307 ' In the case of an invalid action being passed, these will have a value',
1308 ' of "unknown_action".',
1309 '',
1310 ' For more information see https://www.mediawiki.org' .
1311 '/wiki/API:Errors_and_warnings',
1312 '',
1313 'Documentation: https://www.mediawiki.org/wiki/API:Main_page',
1314 'FAQ https://www.mediawiki.org/wiki/API:FAQ',
1315 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
1316 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
1317 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' .
1318 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
1319 '',
1320 '',
1321 '',
1322 '',
1323 '',
1324 );
1325 }
1326
1327 /**
1328 * Sets whether the pretty-printer should format *bold* and $italics$
1329 *
1330 * @deprecated since 1.25
1331 * @param bool $help
1332 */
1333 public function setHelp( $help = true ) {
1334 wfDeprecated( __METHOD__, '1.25' );
1335 $this->mPrinter->setHelp( $help );
1336 }
1337
1338 /**
1339 * Override the parent to generate help messages for all available modules.
1340 *
1341 * @deprecated since 1.25
1342 * @return string
1343 */
1344 public function makeHelpMsg() {
1345 wfDeprecated( __METHOD__, '1.25' );
1346 global $wgMemc;
1347 $this->setHelp();
1348 // Get help text from cache if present
1349 $key = wfMemcKey( 'apihelp', $this->getModuleName(),
1350 str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
1351
1352 $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
1353 if ( $cacheHelpTimeout > 0 ) {
1354 $cached = $wgMemc->get( $key );
1355 if ( $cached ) {
1356 return $cached;
1357 }
1358 }
1359 $retval = $this->reallyMakeHelpMsg();
1360 if ( $cacheHelpTimeout > 0 ) {
1361 $wgMemc->set( $key, $retval, $cacheHelpTimeout );
1362 }
1363
1364 return $retval;
1365 }
1366
1367 /**
1368 * @deprecated since 1.25
1369 * @return mixed|string
1370 */
1371 public function reallyMakeHelpMsg() {
1372 wfDeprecated( __METHOD__, '1.25' );
1373 $this->setHelp();
1374
1375 // Use parent to make default message for the main module
1376 $msg = parent::makeHelpMsg();
1377
1378 $astriks = str_repeat( '*** ', 14 );
1379 $msg .= "\n\n$astriks Modules $astriks\n\n";
1380
1381 foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
1382 $module = $this->mModuleMgr->getModule( $name );
1383 $msg .= self::makeHelpMsgHeader( $module, 'action' );
1384
1385 $msg2 = $module->makeHelpMsg();
1386 if ( $msg2 !== false ) {
1387 $msg .= $msg2;
1388 }
1389 $msg .= "\n";
1390 }
1391
1392 $msg .= "\n$astriks Permissions $astriks\n\n";
1393 foreach ( self::$mRights as $right => $rightMsg ) {
1394 $rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
1395 ->useDatabase( false )
1396 ->inLanguage( 'en' )
1397 ->text();
1398 $groups = User::getGroupsWithPermission( $right );
1399 $msg .= "* " . $right . " *\n $rightsMsg" .
1400 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
1401 }
1402
1403 $msg .= "\n$astriks Formats $astriks\n\n";
1404 foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
1405 $module = $this->mModuleMgr->getModule( $name );
1406 $msg .= self::makeHelpMsgHeader( $module, 'format' );
1407 $msg2 = $module->makeHelpMsg();
1408 if ( $msg2 !== false ) {
1409 $msg .= $msg2;
1410 }
1411 $msg .= "\n";
1412 }
1413
1414 $credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
1415 $credits = str_replace( "\n", "\n ", $credits );
1416 $msg .= "\n*** Credits: ***\n $credits\n";
1417
1418 return $msg;
1419 }
1420
1421 /**
1422 * @deprecated since 1.25
1423 * @param ApiBase $module
1424 * @param string $paramName What type of request is this? e.g. action,
1425 * query, list, prop, meta, format
1426 * @return string
1427 */
1428 public static function makeHelpMsgHeader( $module, $paramName ) {
1429 wfDeprecated( __METHOD__, '1.25' );
1430 $modulePrefix = $module->getModulePrefix();
1431 if ( strval( $modulePrefix ) !== '' ) {
1432 $modulePrefix = "($modulePrefix) ";
1433 }
1434
1435 return "* $paramName={$module->getModuleName()} $modulePrefix*";
1436 }
1437
1438 /**
1439 * Check whether the user wants us to show version information in the API help
1440 * @return bool
1441 * @deprecated since 1.21, always returns false
1442 */
1443 public function getShowVersions() {
1444 wfDeprecated( __METHOD__, '1.21' );
1445
1446 return false;
1447 }
1448
1449 /**
1450 * Add or overwrite a module in this ApiMain instance. Intended for use by extending
1451 * classes who wish to add their own modules to their lexicon or override the
1452 * behavior of inherent ones.
1453 *
1454 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1455 * @param string $name The identifier for this module.
1456 * @param ApiBase $class The class where this module is implemented.
1457 */
1458 protected function addModule( $name, $class ) {
1459 $this->getModuleManager()->addModule( $name, 'action', $class );
1460 }
1461
1462 /**
1463 * Add or overwrite an output format for this ApiMain. Intended for use by extending
1464 * classes who wish to add to or modify current formatters.
1465 *
1466 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1467 * @param string $name The identifier for this format.
1468 * @param ApiFormatBase $class The class implementing this format.
1469 */
1470 protected function addFormat( $name, $class ) {
1471 $this->getModuleManager()->addModule( $name, 'format', $class );
1472 }
1473
1474 /**
1475 * Get the array mapping module names to class names
1476 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1477 * @return array
1478 */
1479 function getModules() {
1480 return $this->getModuleManager()->getNamesWithClasses( 'action' );
1481 }
1482
1483 /**
1484 * Returns the list of supported formats in form ( 'format' => 'ClassName' )
1485 *
1486 * @since 1.18
1487 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1488 * @return array
1489 */
1490 public function getFormats() {
1491 return $this->getModuleManager()->getNamesWithClasses( 'format' );
1492 }
1493
1494 /**@}*/
1495
1496 }
1497
1498 /**
1499 * This exception will be thrown when dieUsage is called to stop module execution.
1500 *
1501 * @ingroup API
1502 */
1503 class UsageException extends MWException {
1504
1505 private $mCodestr;
1506
1507 /**
1508 * @var null|array
1509 */
1510 private $mExtraData;
1511
1512 /**
1513 * @param string $message
1514 * @param string $codestr
1515 * @param int $code
1516 * @param array|null $extradata
1517 */
1518 public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1519 parent::__construct( $message, $code );
1520 $this->mCodestr = $codestr;
1521 $this->mExtraData = $extradata;
1522 }
1523
1524 /**
1525 * @return string
1526 */
1527 public function getCodeString() {
1528 return $this->mCodestr;
1529 }
1530
1531 /**
1532 * @return array
1533 */
1534 public function getMessageArray() {
1535 $result = array(
1536 'code' => $this->mCodestr,
1537 'info' => $this->getMessage()
1538 );
1539 if ( is_array( $this->mExtraData ) ) {
1540 $result = array_merge( $result, $this->mExtraData );
1541 }
1542
1543 return $result;
1544 }
1545
1546 /**
1547 * @return string
1548 */
1549 public function __toString() {
1550 return "{$this->getCodeString()}: {$this->getMessage()}";
1551 }
1552 }
1553
1554 /**
1555 * For really cool vim folding this needs to be at the end:
1556 * vim: foldmarker=@{,@} foldmethod=marker
1557 */