836853d925282fbdfa91f23c71a04911ba07ba2a
[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 = 'xmlfm';
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 if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) {
400 echo wfReportTime();
401 }
402
403 ob_end_flush();
404 }
405
406 /**
407 * Handle an exception as an API response
408 *
409 * @since 1.23
410 * @param Exception $e
411 */
412 protected function handleException( Exception $e ) {
413 // Bug 63145: Rollback any open database transactions
414 if ( !( $e instanceof UsageException ) ) {
415 // UsageExceptions are intentional, so don't rollback if that's the case
416 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
417 }
418
419 // Allow extra cleanup and logging
420 wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
421
422 // Log it
423 if ( !( $e instanceof UsageException ) ) {
424 MWExceptionHandler::logException( $e );
425 }
426
427 // Handle any kind of exception by outputting properly formatted error message.
428 // If this fails, an unhandled exception should be thrown so that global error
429 // handler will process and log it.
430
431 $errCode = $this->substituteResultWithError( $e );
432
433 // Error results should not be cached
434 $this->setCacheMode( 'private' );
435
436 $response = $this->getRequest()->response();
437 $headerStr = 'MediaWiki-API-Error: ' . $errCode;
438 if ( $e->getCode() === 0 ) {
439 $response->header( $headerStr );
440 } else {
441 $response->header( $headerStr, true, $e->getCode() );
442 }
443
444 // Reset and print just the error message
445 ob_clean();
446
447 // If the error occurred during printing, do a printer->profileOut()
448 $this->mPrinter->safeProfileOut();
449 $this->printResult( true );
450 }
451
452 /**
453 * Handle an exception from the ApiBeforeMain hook.
454 *
455 * This tries to print the exception as an API response, to be more
456 * friendly to clients. If it fails, it will rethrow the exception.
457 *
458 * @since 1.23
459 * @param Exception $e
460 */
461 public static function handleApiBeforeMainException( Exception $e ) {
462 ob_start();
463
464 try {
465 $main = new self( RequestContext::getMain(), false );
466 $main->handleException( $e );
467 } catch ( Exception $e2 ) {
468 // Nope, even that didn't work. Punt.
469 throw $e;
470 }
471
472 // Log the request and reset cache headers
473 $main->logRequest( 0 );
474 $main->sendCacheHeaders();
475
476 ob_end_flush();
477 }
478
479 /**
480 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
481 *
482 * If no origin parameter is present, nothing happens.
483 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
484 * is set and false is returned.
485 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
486 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
487 * headers are set.
488 *
489 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
490 */
491 protected function handleCORS() {
492 $originParam = $this->getParameter( 'origin' ); // defaults to null
493 if ( $originParam === null ) {
494 // No origin parameter, nothing to do
495 return true;
496 }
497
498 $request = $this->getRequest();
499 $response = $request->response();
500 // Origin: header is a space-separated list of origins, check all of them
501 $originHeader = $request->getHeader( 'Origin' );
502 if ( $originHeader === false ) {
503 $origins = array();
504 } else {
505 $origins = explode( ' ', $originHeader );
506 }
507
508 if ( !in_array( $originParam, $origins ) ) {
509 // origin parameter set but incorrect
510 // Send a 403 response
511 $message = HttpStatus::getMessage( 403 );
512 $response->header( "HTTP/1.1 403 $message", true, 403 );
513 $response->header( 'Cache-Control: no-cache' );
514 echo "'origin' parameter does not match Origin header\n";
515
516 return false;
517 }
518
519 $config = $this->getConfig();
520 $matchOrigin = self::matchOrigin(
521 $originParam,
522 $config->get( 'CrossSiteAJAXdomains' ),
523 $config->get( 'CrossSiteAJAXdomainExceptions' )
524 );
525
526 if ( $matchOrigin ) {
527 $response->header( "Access-Control-Allow-Origin: $originParam" );
528 $response->header( 'Access-Control-Allow-Credentials: true' );
529 $this->getOutput()->addVaryHeader( 'Origin' );
530 }
531
532 return true;
533 }
534
535 /**
536 * Attempt to match an Origin header against a set of rules and a set of exceptions
537 * @param string $value Origin header
538 * @param array $rules Set of wildcard rules
539 * @param array $exceptions Set of wildcard rules
540 * @return bool True if $value matches a rule in $rules and doesn't match
541 * any rules in $exceptions, false otherwise
542 */
543 protected static function matchOrigin( $value, $rules, $exceptions ) {
544 foreach ( $rules as $rule ) {
545 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
546 // Rule matches, check exceptions
547 foreach ( $exceptions as $exc ) {
548 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
549 return false;
550 }
551 }
552
553 return true;
554 }
555 }
556
557 return false;
558 }
559
560 /**
561 * Helper function to convert wildcard string into a regex
562 * '*' => '.*?'
563 * '?' => '.'
564 *
565 * @param string $wildcard String with wildcards
566 * @return string Regular expression
567 */
568 protected static function wildcardToRegex( $wildcard ) {
569 $wildcard = preg_quote( $wildcard, '/' );
570 $wildcard = str_replace(
571 array( '\*', '\?' ),
572 array( '.*?', '.' ),
573 $wildcard
574 );
575
576 return "/https?:\/\/$wildcard/";
577 }
578
579 protected function sendCacheHeaders() {
580 $response = $this->getRequest()->response();
581 $out = $this->getOutput();
582
583 $config = $this->getConfig();
584
585 if ( $config->get( 'VaryOnXFP' ) ) {
586 $out->addVaryHeader( 'X-Forwarded-Proto' );
587 }
588
589 if ( $this->mCacheMode == 'private' ) {
590 $response->header( 'Cache-Control: private' );
591 return;
592 }
593
594 $useXVO = $config->get( 'UseXVO' );
595 if ( $this->mCacheMode == 'anon-public-user-private' ) {
596 $out->addVaryHeader( 'Cookie' );
597 $response->header( $out->getVaryHeader() );
598 if ( $useXVO ) {
599 $response->header( $out->getXVO() );
600 if ( $out->haveCacheVaryCookies() ) {
601 // Logged in, mark this request private
602 $response->header( 'Cache-Control: private' );
603 return;
604 }
605 // Logged out, send normal public headers below
606 } elseif ( session_id() != '' ) {
607 // Logged in or otherwise has session (e.g. anonymous users who have edited)
608 // Mark request private
609 $response->header( 'Cache-Control: private' );
610
611 return;
612 } // else no XVO and anonymous, send public headers below
613 }
614
615 // Send public headers
616 $response->header( $out->getVaryHeader() );
617 if ( $useXVO ) {
618 $response->header( $out->getXVO() );
619 }
620
621 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
622 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
623 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
624 }
625 if ( !isset( $this->mCacheControl['max-age'] ) ) {
626 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
627 }
628
629 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
630 // Public cache not requested
631 // Sending a Vary header in this case is harmless, and protects us
632 // against conditional calls of setCacheMaxAge().
633 $response->header( 'Cache-Control: private' );
634
635 return;
636 }
637
638 $this->mCacheControl['public'] = true;
639
640 // Send an Expires header
641 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
642 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
643 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
644
645 // Construct the Cache-Control header
646 $ccHeader = '';
647 $separator = '';
648 foreach ( $this->mCacheControl as $name => $value ) {
649 if ( is_bool( $value ) ) {
650 if ( $value ) {
651 $ccHeader .= $separator . $name;
652 $separator = ', ';
653 }
654 } else {
655 $ccHeader .= $separator . "$name=$value";
656 $separator = ', ';
657 }
658 }
659
660 $response->header( "Cache-Control: $ccHeader" );
661 }
662
663 /**
664 * Replace the result data with the information about an exception.
665 * Returns the error code
666 * @param Exception $e
667 * @return string
668 */
669 protected function substituteResultWithError( $e ) {
670 $result = $this->getResult();
671
672 // Printer may not be initialized if the extractRequestParams() fails for the main module
673 if ( !isset( $this->mPrinter ) ) {
674 // The printer has not been created yet. Try to manually get formatter value.
675 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
676 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
677 $value = self::API_DEFAULT_FORMAT;
678 }
679
680 $this->mPrinter = $this->createPrinterByName( $value );
681 }
682
683 // Printer may not be able to handle errors. This is particularly
684 // likely if the module returns something for getCustomPrinter().
685 if ( !$this->mPrinter->canPrintErrors() ) {
686 $this->mPrinter->safeProfileOut();
687 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
688 }
689
690 // Update raw mode flag for the selected printer.
691 $result->setRawMode( $this->mPrinter->getNeedsRawData() );
692
693 $config = $this->getConfig();
694
695 if ( $e instanceof UsageException ) {
696 // User entered incorrect parameters - generate error response
697 $errMessage = $e->getMessageArray();
698 $link = wfExpandUrl( wfScript( 'api' ) );
699 ApiResult::setContent( $errMessage, "See $link for API usage" );
700 } else {
701 // Something is seriously wrong
702 if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
703 $info = 'Database query error';
704 } else {
705 $info = "Exception Caught: {$e->getMessage()}";
706 }
707
708 $errMessage = array(
709 'code' => 'internal_api_error_' . get_class( $e ),
710 'info' => $info,
711 );
712 ApiResult::setContent(
713 $errMessage,
714 $config->get( 'ShowExceptionDetails' ) ? "\n\n{$e->getTraceAsString()}\n\n" : ''
715 );
716 }
717
718 // Remember all the warnings to re-add them later
719 $oldResult = $result->getData();
720 $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
721
722 $result->reset();
723 // Re-add the id
724 $requestid = $this->getParameter( 'requestid' );
725 if ( !is_null( $requestid ) ) {
726 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
727 }
728 if ( $config->get( 'ShowHostnames' ) ) {
729 // servedby is especially useful when debugging errors
730 $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK );
731 }
732 if ( $warnings !== null ) {
733 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
734 }
735
736 $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
737
738 return $errMessage['code'];
739 }
740
741 /**
742 * Set up for the execution.
743 * @return array
744 */
745 protected function setupExecuteAction() {
746 // First add the id to the top element
747 $result = $this->getResult();
748 $requestid = $this->getParameter( 'requestid' );
749 if ( !is_null( $requestid ) ) {
750 $result->addValue( null, 'requestid', $requestid );
751 }
752
753 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
754 $servedby = $this->getParameter( 'servedby' );
755 if ( $servedby ) {
756 $result->addValue( null, 'servedby', wfHostName() );
757 }
758 }
759
760 if ( $this->getParameter( 'curtimestamp' ) ) {
761 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
762 ApiResult::NO_SIZE_CHECK );
763 }
764
765 $params = $this->extractRequestParams();
766
767 $this->mAction = $params['action'];
768
769 if ( !is_string( $this->mAction ) ) {
770 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
771 }
772
773 return $params;
774 }
775
776 /**
777 * Set up the module for response
778 * @return ApiBase The module that will handle this action
779 */
780 protected function setupModule() {
781 // Instantiate the module requested by the user
782 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
783 if ( $module === null ) {
784 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
785 }
786 $moduleParams = $module->extractRequestParams();
787
788 // Check token, if necessary
789 if ( $module->needsToken() === true ) {
790 throw new MWException(
791 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
792 "See documentation for ApiBase::needsToken for details."
793 );
794 }
795 if ( $module->needsToken() ) {
796 if ( !$module->mustBePosted() ) {
797 throw new MWException(
798 "Module '{$module->getModuleName()}' must require POST to use tokens."
799 );
800 }
801
802 if ( !isset( $moduleParams['token'] ) ) {
803 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
804 }
805
806 if ( !$this->getConfig()->get( 'DebugAPI' ) &&
807 array_key_exists(
808 $module->encodeParamName( 'token' ),
809 $this->getRequest()->getQueryValues()
810 )
811 ) {
812 $this->dieUsage(
813 "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body",
814 'mustposttoken'
815 );
816 }
817
818 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
819 $this->dieUsageMsg( 'sessionfailure' );
820 }
821 }
822
823 return $module;
824 }
825
826 /**
827 * Check the max lag if necessary
828 * @param ApiBase $module Api module being used
829 * @param array $params Array an array containing the request parameters.
830 * @return bool True on success, false should exit immediately
831 */
832 protected function checkMaxLag( $module, $params ) {
833 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
834 // Check for maxlag
835 $maxLag = $params['maxlag'];
836 list( $host, $lag ) = wfGetLB()->getMaxLag();
837 if ( $lag > $maxLag ) {
838 $response = $this->getRequest()->response();
839
840 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
841 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
842
843 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
844 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
845 }
846
847 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
848 }
849 }
850
851 return true;
852 }
853
854 /**
855 * Check for sufficient permissions to execute
856 * @param ApiBase $module An Api module
857 */
858 protected function checkExecutePermissions( $module ) {
859 $user = $this->getUser();
860 if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
861 !$user->isAllowed( 'read' )
862 ) {
863 $this->dieUsageMsg( 'readrequired' );
864 }
865 if ( $module->isWriteMode() ) {
866 if ( !$this->mEnableWrite ) {
867 $this->dieUsageMsg( 'writedisabled' );
868 }
869 if ( !$user->isAllowed( 'writeapi' ) ) {
870 $this->dieUsageMsg( 'writerequired' );
871 }
872 if ( wfReadOnly() ) {
873 $this->dieReadOnly();
874 }
875 }
876
877 // Allow extensions to stop execution for arbitrary reasons.
878 $message = false;
879 if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
880 $this->dieUsageMsg( $message );
881 }
882 }
883
884 /**
885 * Check asserts of the user's rights
886 * @param array $params
887 */
888 protected function checkAsserts( $params ) {
889 if ( isset( $params['assert'] ) ) {
890 $user = $this->getUser();
891 switch ( $params['assert'] ) {
892 case 'user':
893 if ( $user->isAnon() ) {
894 $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
895 }
896 break;
897 case 'bot':
898 if ( !$user->isAllowed( 'bot' ) ) {
899 $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
900 }
901 break;
902 }
903 }
904 }
905
906 /**
907 * Check POST for external response and setup result printer
908 * @param ApiBase $module An Api module
909 * @param array $params An array with the request parameters
910 */
911 protected function setupExternalResponse( $module, $params ) {
912 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
913 // Module requires POST. GET request might still be allowed
914 // if $wgDebugApi is true, otherwise fail.
915 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) );
916 }
917
918 // See if custom printer is used
919 $this->mPrinter = $module->getCustomPrinter();
920 if ( is_null( $this->mPrinter ) ) {
921 // Create an appropriate printer
922 $this->mPrinter = $this->createPrinterByName( $params['format'] );
923 }
924
925 if ( $this->mPrinter->getNeedsRawData() ) {
926 $this->getResult()->setRawMode();
927 }
928 }
929
930 /**
931 * Execute the actual module, without any error handling
932 */
933 protected function executeAction() {
934 $params = $this->setupExecuteAction();
935 $module = $this->setupModule();
936 $this->mModule = $module;
937
938 $this->checkExecutePermissions( $module );
939
940 if ( !$this->checkMaxLag( $module, $params ) ) {
941 return;
942 }
943
944 if ( !$this->mInternalMode ) {
945 $this->setupExternalResponse( $module, $params );
946 }
947
948 $this->checkAsserts( $params );
949
950 // Execute
951 $module->profileIn();
952 $module->execute();
953 wfRunHooks( 'APIAfterExecute', array( &$module ) );
954 $module->profileOut();
955
956 $this->reportUnusedParams();
957
958 if ( !$this->mInternalMode ) {
959 //append Debug information
960 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
961
962 // Print result data
963 $this->printResult( false );
964 }
965 }
966
967 /**
968 * Log the preceding request
969 * @param int $time Time in seconds
970 */
971 protected function logRequest( $time ) {
972 $request = $this->getRequest();
973 $milliseconds = $time === null ? '?' : round( $time * 1000 );
974 $s = 'API' .
975 ' ' . $request->getMethod() .
976 ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
977 ' ' . $request->getIP() .
978 ' T=' . $milliseconds . 'ms';
979 foreach ( $this->getParamsUsed() as $name ) {
980 $value = $request->getVal( $name );
981 if ( $value === null ) {
982 continue;
983 }
984 $s .= ' ' . $name . '=';
985 if ( strlen( $value ) > 256 ) {
986 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
987 $s .= $encValue . '[...]';
988 } else {
989 $s .= $this->encodeRequestLogValue( $value );
990 }
991 }
992 $s .= "\n";
993 wfDebugLog( 'api', $s, 'private' );
994 }
995
996 /**
997 * Encode a value in a format suitable for a space-separated log line.
998 * @param string $s
999 * @return string
1000 */
1001 protected function encodeRequestLogValue( $s ) {
1002 static $table;
1003 if ( !$table ) {
1004 $chars = ';@$!*(),/:';
1005 $numChars = strlen( $chars );
1006 for ( $i = 0; $i < $numChars; $i++ ) {
1007 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1008 }
1009 }
1010
1011 return strtr( rawurlencode( $s ), $table );
1012 }
1013
1014 /**
1015 * Get the request parameters used in the course of the preceding execute() request
1016 * @return array
1017 */
1018 protected function getParamsUsed() {
1019 return array_keys( $this->mParamsUsed );
1020 }
1021
1022 /**
1023 * Get a request value, and register the fact that it was used, for logging.
1024 * @param string $name
1025 * @param mixed $default
1026 * @return mixed
1027 */
1028 public function getVal( $name, $default = null ) {
1029 $this->mParamsUsed[$name] = true;
1030
1031 $ret = $this->getRequest()->getVal( $name );
1032 if ( $ret === null ) {
1033 if ( $this->getRequest()->getArray( $name ) !== null ) {
1034 // See bug 10262 for why we don't just join( '|', ... ) the
1035 // array.
1036 $this->setWarning(
1037 "Parameter '$name' uses unsupported PHP array syntax"
1038 );
1039 }
1040 $ret = $default;
1041 }
1042 return $ret;
1043 }
1044
1045 /**
1046 * Get a boolean request value, and register the fact that the parameter
1047 * was used, for logging.
1048 * @param string $name
1049 * @return bool
1050 */
1051 public function getCheck( $name ) {
1052 return $this->getVal( $name, null ) !== null;
1053 }
1054
1055 /**
1056 * Get a request upload, and register the fact that it was used, for logging.
1057 *
1058 * @since 1.21
1059 * @param string $name Parameter name
1060 * @return WebRequestUpload
1061 */
1062 public function getUpload( $name ) {
1063 $this->mParamsUsed[$name] = true;
1064
1065 return $this->getRequest()->getUpload( $name );
1066 }
1067
1068 /**
1069 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
1070 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
1071 */
1072 protected function reportUnusedParams() {
1073 $paramsUsed = $this->getParamsUsed();
1074 $allParams = $this->getRequest()->getValueNames();
1075
1076 if ( !$this->mInternalMode ) {
1077 // Printer has not yet executed; don't warn that its parameters are unused
1078 $printerParams = array_map(
1079 array( $this->mPrinter, 'encodeParamName' ),
1080 array_keys( $this->mPrinter->getFinalParams() ?: array() )
1081 );
1082 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1083 } else {
1084 $unusedParams = array_diff( $allParams, $paramsUsed );
1085 }
1086
1087 if ( count( $unusedParams ) ) {
1088 $s = count( $unusedParams ) > 1 ? 's' : '';
1089 $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
1090 }
1091 }
1092
1093 /**
1094 * Print results using the current printer
1095 *
1096 * @param bool $isError
1097 */
1098 protected function printResult( $isError ) {
1099 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1100 $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
1101 }
1102
1103 $this->getResult()->cleanUpUTF8();
1104 $printer = $this->mPrinter;
1105 $printer->profileIn();
1106
1107 $printer->initPrinter( false );
1108
1109 $printer->execute();
1110 $printer->closePrinter();
1111 $printer->profileOut();
1112 }
1113
1114 /**
1115 * @return bool
1116 */
1117 public function isReadMode() {
1118 return false;
1119 }
1120
1121 /**
1122 * See ApiBase for description.
1123 *
1124 * @return array
1125 */
1126 public function getAllowedParams() {
1127 global $wgContLang;
1128
1129 return array(
1130 'action' => array(
1131 ApiBase::PARAM_DFLT => 'help',
1132 ApiBase::PARAM_TYPE => 'submodule',
1133 ),
1134 'format' => array(
1135 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
1136 ApiBase::PARAM_TYPE => 'submodule',
1137 ),
1138 'maxlag' => array(
1139 ApiBase::PARAM_TYPE => 'integer'
1140 ),
1141 'smaxage' => array(
1142 ApiBase::PARAM_TYPE => 'integer',
1143 ApiBase::PARAM_DFLT => 0
1144 ),
1145 'maxage' => array(
1146 ApiBase::PARAM_TYPE => 'integer',
1147 ApiBase::PARAM_DFLT => 0
1148 ),
1149 'assert' => array(
1150 ApiBase::PARAM_TYPE => array( 'user', 'bot' )
1151 ),
1152 'requestid' => null,
1153 'servedby' => false,
1154 'curtimestamp' => false,
1155 'origin' => null,
1156 'uselang' => array(
1157 ApiBase::PARAM_DFLT => $wgContLang->getCode(),
1158 ),
1159 );
1160 }
1161
1162 /** @see ApiBase::getExamplesMessages() */
1163 public function getExamplesMessages() {
1164 return array(
1165 'action=help' => 'apihelp-help-example-main',
1166 'action=help&recursivesubmodules=1' => 'apihelp-help-example-recursive',
1167 );
1168 }
1169
1170 public function modifyHelp( array &$help, array $options ) {
1171 // Wish PHP had an "array_insert_before". Instead, we have to manually
1172 // reindex the array to get 'permissions' in the right place.
1173 $oldHelp = $help;
1174 $help = array();
1175 foreach ( $oldHelp as $k => $v ) {
1176 if ( $k === 'submodules' ) {
1177 $help['permissions'] = '';
1178 }
1179 $help[$k] = $v;
1180 }
1181 $help['credits'] = '';
1182
1183 // Fill 'permissions'
1184 $help['permissions'] .= Html::openElement( 'div',
1185 array( 'class' => 'apihelp-block apihelp-permissions' ) );
1186 $m = $this->msg( 'api-help-permissions' );
1187 if ( !$m->isDisabled() ) {
1188 $help['permissions'] .= Html::rawElement( 'div', array( 'class' => 'apihelp-block-head' ),
1189 $m->numParams( count( self::$mRights ) )->parse()
1190 );
1191 }
1192 $help['permissions'] .= Html::openElement( 'dl' );
1193 foreach ( self::$mRights as $right => $rightMsg ) {
1194 $help['permissions'] .= Html::element( 'dt', null, $right );
1195
1196 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1197 $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1198
1199 $groups = array_map( function ( $group ) {
1200 return $group == '*' ? 'all' : $group;
1201 }, User::getGroupsWithPermission( $right ) );
1202
1203 $help['permissions'] .= Html::rawElement( 'dd', null,
1204 $this->msg( 'api-help-permissions-granted-to' )
1205 ->numParams( count( $groups ) )
1206 ->params( $this->getLanguage()->commaList( $groups ) )
1207 ->parse()
1208 );
1209 }
1210 $help['permissions'] .= Html::closeElement( 'dl' );
1211 $help['permissions'] .= Html::closeElement( 'div' );
1212
1213 // Fill 'credits', if applicable
1214 if ( empty( $options['nolead'] ) ) {
1215 $help['credits'] .= Html::element( 'h' . min( 6, $options['headerlevel'] + 1 ),
1216 array( 'id' => '+credits', 'class' => 'apihelp-header' ),
1217 $this->msg( 'api-credits-header' )->parse()
1218 );
1219 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1220 }
1221 }
1222
1223 private $mCanApiHighLimits = null;
1224
1225 /**
1226 * Check whether the current user is allowed to use high limits
1227 * @return bool
1228 */
1229 public function canApiHighLimits() {
1230 if ( !isset( $this->mCanApiHighLimits ) ) {
1231 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
1232 }
1233
1234 return $this->mCanApiHighLimits;
1235 }
1236
1237 /**
1238 * Overrides to return this instance's module manager.
1239 * @return ApiModuleManager
1240 */
1241 public function getModuleManager() {
1242 return $this->mModuleMgr;
1243 }
1244
1245 /************************************************************************//**
1246 * @name Deprecated
1247 * @{
1248 */
1249
1250 /**
1251 * @deprecated since 1.25
1252 * @return array
1253 */
1254 public function getParamDescription() {
1255 return array(
1256 'format' => 'The format of the output',
1257 'action' => 'What action you would like to perform. See below for module help',
1258 'maxlag' => array(
1259 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
1260 'To save actions causing any more site replication lag, this parameter can make the client',
1261 'wait until the replication lag is less than the specified value.',
1262 'In case of a replag error, error code "maxlag" is returned, with the message like',
1263 '"Waiting for $host: $lag seconds lagged\n".',
1264 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
1265 ),
1266 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
1267 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
1268 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"',
1269 'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
1270 'servedby' => 'Include the hostname that served the request in the ' .
1271 'results. Unconditionally shown on error',
1272 'curtimestamp' => 'Include the current timestamp in the result.',
1273 'origin' => array(
1274 'When accessing the API using a cross-domain AJAX request (CORS), set this to the',
1275 'originating domain. This must be included in any pre-flight request, and',
1276 'therefore must be part of the request URI (not the POST body). This must match',
1277 'one of the origins in the Origin: header exactly, so it has to be set to ',
1278 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this',
1279 'parameter does not match the Origin: header, a 403 response will be returned. If',
1280 'this parameter matches the Origin: header and the origin is whitelisted, an',
1281 'Access-Control-Allow-Origin header will be set.',
1282 ),
1283 );
1284 }
1285
1286 /**
1287 * @deprecated since 1.25
1288 * @return array
1289 */
1290 public function getDescription() {
1291 return array(
1292 '',
1293 '',
1294 '**********************************************************************************************',
1295 '** **',
1296 '** This is an auto-generated MediaWiki API documentation page **',
1297 '** **',
1298 '** Documentation and Examples: **',
1299 '** https://www.mediawiki.org/wiki/API **',
1300 '** **',
1301 '**********************************************************************************************',
1302 '',
1303 'Status: All features shown on this page should be working, but the API',
1304 ' is still in active development, and may change at any time.',
1305 ' Make sure to monitor our mailing list for any updates.',
1306 '',
1307 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent',
1308 ' with the key "MediaWiki-API-Error" and then both the value of the',
1309 ' header and the error code sent back will be set to the same value.',
1310 '',
1311 ' In the case of an invalid action being passed, these will have a value',
1312 ' of "unknown_action".',
1313 '',
1314 ' For more information see https://www.mediawiki.org' .
1315 '/wiki/API:Errors_and_warnings',
1316 '',
1317 'Documentation: https://www.mediawiki.org/wiki/API:Main_page',
1318 'FAQ https://www.mediawiki.org/wiki/API:FAQ',
1319 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
1320 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
1321 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' .
1322 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
1323 '',
1324 '',
1325 '',
1326 '',
1327 '',
1328 );
1329 }
1330
1331 /**
1332 * Sets whether the pretty-printer should format *bold* and $italics$
1333 *
1334 * @deprecated since 1.25
1335 * @param bool $help
1336 */
1337 public function setHelp( $help = true ) {
1338 wfDeprecated( __METHOD__, '1.25' );
1339 $this->mPrinter->setHelp( $help );
1340 }
1341
1342 /**
1343 * Override the parent to generate help messages for all available modules.
1344 *
1345 * @deprecated since 1.25
1346 * @return string
1347 */
1348 public function makeHelpMsg() {
1349 wfDeprecated( __METHOD__, '1.25' );
1350 global $wgMemc;
1351 $this->setHelp();
1352 // Get help text from cache if present
1353 $key = wfMemcKey( 'apihelp', $this->getModuleName(),
1354 str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
1355
1356 $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
1357 if ( $cacheHelpTimeout > 0 ) {
1358 $cached = $wgMemc->get( $key );
1359 if ( $cached ) {
1360 return $cached;
1361 }
1362 }
1363 $retval = $this->reallyMakeHelpMsg();
1364 if ( $cacheHelpTimeout > 0 ) {
1365 $wgMemc->set( $key, $retval, $cacheHelpTimeout );
1366 }
1367
1368 return $retval;
1369 }
1370
1371 /**
1372 * @deprecated since 1.25
1373 * @return mixed|string
1374 */
1375 public function reallyMakeHelpMsg() {
1376 wfDeprecated( __METHOD__, '1.25' );
1377 $this->setHelp();
1378
1379 // Use parent to make default message for the main module
1380 $msg = parent::makeHelpMsg();
1381
1382 $astriks = str_repeat( '*** ', 14 );
1383 $msg .= "\n\n$astriks Modules $astriks\n\n";
1384
1385 foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
1386 $module = $this->mModuleMgr->getModule( $name );
1387 $msg .= self::makeHelpMsgHeader( $module, 'action' );
1388
1389 $msg2 = $module->makeHelpMsg();
1390 if ( $msg2 !== false ) {
1391 $msg .= $msg2;
1392 }
1393 $msg .= "\n";
1394 }
1395
1396 $msg .= "\n$astriks Permissions $astriks\n\n";
1397 foreach ( self::$mRights as $right => $rightMsg ) {
1398 $rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
1399 ->useDatabase( false )
1400 ->inLanguage( 'en' )
1401 ->text();
1402 $groups = User::getGroupsWithPermission( $right );
1403 $msg .= "* " . $right . " *\n $rightsMsg" .
1404 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
1405 }
1406
1407 $msg .= "\n$astriks Formats $astriks\n\n";
1408 foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
1409 $module = $this->mModuleMgr->getModule( $name );
1410 $msg .= self::makeHelpMsgHeader( $module, 'format' );
1411 $msg2 = $module->makeHelpMsg();
1412 if ( $msg2 !== false ) {
1413 $msg .= $msg2;
1414 }
1415 $msg .= "\n";
1416 }
1417
1418 $credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
1419 $credits = str_replace( "\n", "\n ", $credits );
1420 $msg .= "\n*** Credits: ***\n $credits\n";
1421
1422 return $msg;
1423 }
1424
1425 /**
1426 * @deprecated since 1.25
1427 * @param ApiBase $module
1428 * @param string $paramName What type of request is this? e.g. action,
1429 * query, list, prop, meta, format
1430 * @return string
1431 */
1432 public static function makeHelpMsgHeader( $module, $paramName ) {
1433 wfDeprecated( __METHOD__, '1.25' );
1434 $modulePrefix = $module->getModulePrefix();
1435 if ( strval( $modulePrefix ) !== '' ) {
1436 $modulePrefix = "($modulePrefix) ";
1437 }
1438
1439 return "* $paramName={$module->getModuleName()} $modulePrefix*";
1440 }
1441
1442 /**
1443 * Check whether the user wants us to show version information in the API help
1444 * @return bool
1445 * @deprecated since 1.21, always returns false
1446 */
1447 public function getShowVersions() {
1448 wfDeprecated( __METHOD__, '1.21' );
1449
1450 return false;
1451 }
1452
1453 /**
1454 * Add or overwrite a module in this ApiMain instance. Intended for use by extending
1455 * classes who wish to add their own modules to their lexicon or override the
1456 * behavior of inherent ones.
1457 *
1458 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1459 * @param string $name The identifier for this module.
1460 * @param ApiBase $class The class where this module is implemented.
1461 */
1462 protected function addModule( $name, $class ) {
1463 $this->getModuleManager()->addModule( $name, 'action', $class );
1464 }
1465
1466 /**
1467 * Add or overwrite an output format for this ApiMain. Intended for use by extending
1468 * classes who wish to add to or modify current formatters.
1469 *
1470 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1471 * @param string $name The identifier for this format.
1472 * @param ApiFormatBase $class The class implementing this format.
1473 */
1474 protected function addFormat( $name, $class ) {
1475 $this->getModuleManager()->addModule( $name, 'format', $class );
1476 }
1477
1478 /**
1479 * Get the array mapping module names to class names
1480 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1481 * @return array
1482 */
1483 function getModules() {
1484 return $this->getModuleManager()->getNamesWithClasses( 'action' );
1485 }
1486
1487 /**
1488 * Returns the list of supported formats in form ( 'format' => 'ClassName' )
1489 *
1490 * @since 1.18
1491 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1492 * @return array
1493 */
1494 public function getFormats() {
1495 return $this->getModuleManager()->getNamesWithClasses( 'format' );
1496 }
1497
1498 /**@}*/
1499
1500 }
1501
1502 /**
1503 * This exception will be thrown when dieUsage is called to stop module execution.
1504 *
1505 * @ingroup API
1506 */
1507 class UsageException extends MWException {
1508
1509 private $mCodestr;
1510
1511 /**
1512 * @var null|array
1513 */
1514 private $mExtraData;
1515
1516 /**
1517 * @param string $message
1518 * @param string $codestr
1519 * @param int $code
1520 * @param array|null $extradata
1521 */
1522 public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1523 parent::__construct( $message, $code );
1524 $this->mCodestr = $codestr;
1525 $this->mExtraData = $extradata;
1526 }
1527
1528 /**
1529 * @return string
1530 */
1531 public function getCodeString() {
1532 return $this->mCodestr;
1533 }
1534
1535 /**
1536 * @return array
1537 */
1538 public function getMessageArray() {
1539 $result = array(
1540 'code' => $this->mCodestr,
1541 'info' => $this->getMessage()
1542 );
1543 if ( is_array( $this->mExtraData ) ) {
1544 $result = array_merge( $result, $this->mExtraData );
1545 }
1546
1547 return $result;
1548 }
1549
1550 /**
1551 * @return string
1552 */
1553 public function __toString() {
1554 return "{$this->getCodeString()}: {$this->getMessage()}";
1555 }
1556 }
1557
1558 /**
1559 * For really cool vim folding this needs to be at the end:
1560 * vim: foldmarker=@{,@} foldmethod=marker
1561 */