Merge "Remove unused $wgDebugDBTransactions"
[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 /**
44 * When no format parameter is given, this format will be used
45 */
46 const API_DEFAULT_FORMAT = 'xmlfm';
47
48 /**
49 * List of available modules: action name => module class
50 */
51 private static $Modules = array(
52 'login' => 'ApiLogin',
53 'logout' => 'ApiLogout',
54 'query' => 'ApiQuery',
55 'expandtemplates' => 'ApiExpandTemplates',
56 'parse' => 'ApiParse',
57 'opensearch' => 'ApiOpenSearch',
58 'feedcontributions' => 'ApiFeedContributions',
59 'feedwatchlist' => 'ApiFeedWatchlist',
60 'help' => 'ApiHelp',
61 'paraminfo' => 'ApiParamInfo',
62 'rsd' => 'ApiRsd',
63 'compare' => 'ApiComparePages',
64
65 // Write modules
66 'purge' => 'ApiPurge',
67 'rollback' => 'ApiRollback',
68 'delete' => 'ApiDelete',
69 'undelete' => 'ApiUndelete',
70 'protect' => 'ApiProtect',
71 'block' => 'ApiBlock',
72 'unblock' => 'ApiUnblock',
73 'move' => 'ApiMove',
74 'edit' => 'ApiEditPage',
75 'upload' => 'ApiUpload',
76 'filerevert' => 'ApiFileRevert',
77 'emailuser' => 'ApiEmailUser',
78 'watch' => 'ApiWatch',
79 'patrol' => 'ApiPatrol',
80 'import' => 'ApiImport',
81 'userrights' => 'ApiUserrights',
82 );
83
84 /**
85 * List of available formats: format name => format class
86 */
87 private static $Formats = array(
88 'json' => 'ApiFormatJson',
89 'jsonfm' => 'ApiFormatJson',
90 'php' => 'ApiFormatPhp',
91 'phpfm' => 'ApiFormatPhp',
92 'wddx' => 'ApiFormatWddx',
93 'wddxfm' => 'ApiFormatWddx',
94 'xml' => 'ApiFormatXml',
95 'xmlfm' => 'ApiFormatXml',
96 'yaml' => 'ApiFormatYaml',
97 'yamlfm' => 'ApiFormatYaml',
98 'rawfm' => 'ApiFormatJson',
99 'txt' => 'ApiFormatTxt',
100 'txtfm' => 'ApiFormatTxt',
101 'dbg' => 'ApiFormatDbg',
102 'dbgfm' => 'ApiFormatDbg',
103 'dump' => 'ApiFormatDump',
104 'dumpfm' => 'ApiFormatDump',
105 );
106
107 /**
108 * List of user roles that are specifically relevant to the API.
109 * array( 'right' => array ( 'msg' => 'Some message with a $1',
110 * 'params' => array ( $someVarToSubst ) ),
111 * );
112 */
113 private static $mRights = array(
114 'writeapi' => array(
115 'msg' => 'Use of the write API',
116 'params' => array()
117 ),
118 'apihighlimits' => array(
119 'msg' => 'Use higher limits in API queries (Slow queries: $1 results; Fast queries: $2 results). The limits for slow queries also apply to multivalue parameters.',
120 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 )
121 )
122 );
123
124 /**
125 * @var ApiFormatBase
126 */
127 private $mPrinter;
128
129 private $mModules, $mModuleNames, $mFormats, $mFormatNames;
130 private $mResult, $mAction, $mShowVersions, $mEnableWrite;
131 private $mInternalMode, $mSquidMaxage, $mModule;
132
133 private $mCacheMode = 'private';
134 private $mCacheControl = array();
135
136 /**
137 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
138 *
139 * @param $context IContextSource|WebRequest - if this is an instance of FauxRequest, errors are thrown and no printing occurs
140 * @param $enableWrite bool should be set to true if the api may modify data
141 */
142 public function __construct( $context = null, $enableWrite = false ) {
143 if ( $context === null ) {
144 $context = RequestContext::getMain();
145 } elseif ( $context instanceof WebRequest ) {
146 // BC for pre-1.19
147 $request = $context;
148 $context = RequestContext::getMain();
149 }
150 // We set a derivative context so we can change stuff later
151 $this->setContext( new DerivativeContext( $context ) );
152
153 if ( isset( $request ) ) {
154 $this->getContext()->setRequest( $request );
155 }
156
157 $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
158
159 // Special handling for the main module: $parent === $this
160 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
161
162 if ( !$this->mInternalMode ) {
163 // Impose module restrictions.
164 // If the current user cannot read,
165 // Remove all modules other than login
166 global $wgUser;
167
168 if ( $this->getRequest()->getVal( 'callback' ) !== null ) {
169 // JSON callback allows cross-site reads.
170 // For safety, strip user credentials.
171 wfDebug( "API: stripping user credentials for JSON callback\n" );
172 $wgUser = new User();
173 $this->getContext()->setUser( $wgUser );
174 }
175 }
176
177 global $wgAPIModules; // extension modules
178 $this->mModules = $wgAPIModules + self::$Modules;
179
180 $this->mModuleNames = array_keys( $this->mModules );
181 $this->mFormats = self::$Formats;
182 $this->mFormatNames = array_keys( $this->mFormats );
183
184 $this->mResult = new ApiResult( $this );
185 $this->mShowVersions = false;
186 $this->mEnableWrite = $enableWrite;
187
188 $this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling()
189 $this->mCommit = false;
190 }
191
192 /**
193 * Return true if the API was started by other PHP code using FauxRequest
194 * @return bool
195 */
196 public function isInternalMode() {
197 return $this->mInternalMode;
198 }
199
200 /**
201 * Get the ApiResult object associated with current request
202 *
203 * @return ApiResult
204 */
205 public function getResult() {
206 return $this->mResult;
207 }
208
209 /**
210 * Get the API module object. Only works after executeAction()
211 *
212 * @return ApiBase
213 */
214 public function getModule() {
215 return $this->mModule;
216 }
217
218 /**
219 * Get the result formatter object. Only works after setupExecuteAction()
220 *
221 * @return ApiFormatBase
222 */
223 public function getPrinter() {
224 return $this->mPrinter;
225 }
226
227 /**
228 * Set how long the response should be cached.
229 *
230 * @param $maxage
231 */
232 public function setCacheMaxAge( $maxage ) {
233 $this->setCacheControl( array(
234 'max-age' => $maxage,
235 's-maxage' => $maxage
236 ) );
237 }
238
239 /**
240 * Set the type of caching headers which will be sent.
241 *
242 * @param $mode String One of:
243 * - 'public': Cache this object in public caches, if the maxage or smaxage
244 * parameter is set, or if setCacheMaxAge() was called. If a maximum age is
245 * not provided by any of these means, the object will be private.
246 * - 'private': Cache this object only in private client-side caches.
247 * - 'anon-public-user-private': Make this object cacheable for logged-out
248 * users, but private for logged-in users. IMPORTANT: If this is set, it must be
249 * set consistently for a given URL, it cannot be set differently depending on
250 * things like the contents of the database, or whether the user is logged in.
251 *
252 * If the wiki does not allow anonymous users to read it, the mode set here
253 * will be ignored, and private caching headers will always be sent. In other words,
254 * the "public" mode is equivalent to saying that the data sent is as public as a page
255 * view.
256 *
257 * For user-dependent data, the private mode should generally be used. The
258 * anon-public-user-private mode should only be used where there is a particularly
259 * good performance reason for caching the anonymous response, but where the
260 * response to logged-in users may differ, or may contain private data.
261 *
262 * If this function is never called, then the default will be the private mode.
263 */
264 public function setCacheMode( $mode ) {
265 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) {
266 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
267 // Ignore for forwards-compatibility
268 return;
269 }
270
271 if ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) ) {
272 // Private wiki, only private headers
273 if ( $mode !== 'private' ) {
274 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
275 return;
276 }
277 }
278
279 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
280 $this->mCacheMode = $mode;
281 }
282
283 /**
284 * @deprecated since 1.17 Private caching is now the default, so there is usually no
285 * need to call this function. If there is a need, you can use
286 * $this->setCacheMode('private')
287 */
288 public function setCachePrivate() {
289 wfDeprecated( __METHOD__, '1.17' );
290 $this->setCacheMode( 'private' );
291 }
292
293 /**
294 * Set directives (key/value pairs) for the Cache-Control header.
295 * Boolean values will be formatted as such, by including or omitting
296 * without an equals sign.
297 *
298 * Cache control values set here will only be used if the cache mode is not
299 * private, see setCacheMode().
300 *
301 * @param $directives array
302 */
303 public function setCacheControl( $directives ) {
304 $this->mCacheControl = $directives + $this->mCacheControl;
305 }
306
307 /**
308 * Make sure Vary: Cookie and friends are set. Use this when the output of a request
309 * may be cached for anons but may not be cached for logged-in users.
310 *
311 * WARNING: This function must be called CONSISTENTLY for a given URL. This means that a
312 * given URL must either always or never call this function; if it sometimes does and
313 * sometimes doesn't, stuff will break.
314 *
315 * @deprecated since 1.17 Use setCacheMode( 'anon-public-user-private' )
316 */
317 public function setVaryCookie() {
318 wfDeprecated( __METHOD__, '1.17' );
319 $this->setCacheMode( 'anon-public-user-private' );
320 }
321
322 /**
323 * Create an instance of an output formatter by its name
324 *
325 * @param $format string
326 *
327 * @return ApiFormatBase
328 */
329 public function createPrinterByName( $format ) {
330 if ( !isset( $this->mFormats[$format] ) ) {
331 $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
332 }
333 return new $this->mFormats[$format] ( $this, $format );
334 }
335
336 /**
337 * Execute api request. Any errors will be handled if the API was called by the remote client.
338 */
339 public function execute() {
340 $this->profileIn();
341 if ( $this->mInternalMode ) {
342 $this->executeAction();
343 } else {
344 $this->executeActionWithErrorHandling();
345 }
346
347 $this->profileOut();
348 }
349
350 /**
351 * Execute an action, and in case of an error, erase whatever partial results
352 * have been accumulated, and replace it with an error message and a help screen.
353 */
354 protected function executeActionWithErrorHandling() {
355 // In case an error occurs during data output,
356 // clear the output buffer and print just the error information
357 ob_start();
358
359 try {
360 $this->executeAction();
361 } catch ( Exception $e ) {
362 // Log it
363 if ( $e instanceof MWException ) {
364 wfDebugLog( 'exception', $e->getLogMessage() );
365 }
366
367 // Handle any kind of exception by outputing properly formatted error message.
368 // If this fails, an unhandled exception should be thrown so that global error
369 // handler will process and log it.
370
371 $errCode = $this->substituteResultWithError( $e );
372
373 // Error results should not be cached
374 $this->setCacheMode( 'private' );
375
376 $response = $this->getRequest()->response();
377 $headerStr = 'MediaWiki-API-Error: ' . $errCode;
378 if ( $e->getCode() === 0 ) {
379 $response->header( $headerStr );
380 } else {
381 $response->header( $headerStr, true, $e->getCode() );
382 }
383
384 // Reset and print just the error message
385 ob_clean();
386
387 // If the error occured during printing, do a printer->profileOut()
388 $this->mPrinter->safeProfileOut();
389 $this->printResult( true );
390 }
391
392 // Send cache headers after any code which might generate an error, to
393 // avoid sending public cache headers for errors.
394 $this->sendCacheHeaders();
395
396 if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) {
397 echo wfReportTime();
398 }
399
400 ob_end_flush();
401 }
402
403 protected function sendCacheHeaders() {
404 global $wgUseXVO, $wgVaryOnXFP;
405 $response = $this->getRequest()->response();
406
407 if ( $this->mCacheMode == 'private' ) {
408 $response->header( 'Cache-Control: private' );
409 return;
410 }
411
412 if ( $this->mCacheMode == 'anon-public-user-private' ) {
413 $xfp = $wgVaryOnXFP ? ', X-Forwarded-Proto' : '';
414 $response->header( 'Vary: Accept-Encoding, Cookie' . $xfp );
415 if ( $wgUseXVO ) {
416 $out = $this->getOutput();
417 if ( $wgVaryOnXFP ) {
418 $out->addVaryHeader( 'X-Forwarded-Proto' );
419 }
420 $response->header( $out->getXVO() );
421 if ( $out->haveCacheVaryCookies() ) {
422 // Logged in, mark this request private
423 $response->header( 'Cache-Control: private' );
424 return;
425 }
426 // Logged out, send normal public headers below
427 } elseif ( session_id() != '' ) {
428 // Logged in or otherwise has session (e.g. anonymous users who have edited)
429 // Mark request private
430 $response->header( 'Cache-Control: private' );
431 return;
432 } // else no XVO and anonymous, send public headers below
433 }
434
435 // Send public headers
436 if ( $wgVaryOnXFP ) {
437 $response->header( 'Vary: Accept-Encoding, X-Forwarded-Proto' );
438 if ( $wgUseXVO ) {
439 // Bleeeeegh. Our header setting system sucks
440 $response->header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip, X-Forwarded-Proto' );
441 }
442 }
443
444 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
445 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
446 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
447 }
448 if ( !isset( $this->mCacheControl['max-age'] ) ) {
449 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
450 }
451
452 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
453 // Public cache not requested
454 // Sending a Vary header in this case is harmless, and protects us
455 // against conditional calls of setCacheMaxAge().
456 $response->header( 'Cache-Control: private' );
457 return;
458 }
459
460 $this->mCacheControl['public'] = true;
461
462 // Send an Expires header
463 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
464 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
465 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
466
467 // Construct the Cache-Control header
468 $ccHeader = '';
469 $separator = '';
470 foreach ( $this->mCacheControl as $name => $value ) {
471 if ( is_bool( $value ) ) {
472 if ( $value ) {
473 $ccHeader .= $separator . $name;
474 $separator = ', ';
475 }
476 } else {
477 $ccHeader .= $separator . "$name=$value";
478 $separator = ', ';
479 }
480 }
481
482 $response->header( "Cache-Control: $ccHeader" );
483 }
484
485 /**
486 * Replace the result data with the information about an exception.
487 * Returns the error code
488 * @param $e Exception
489 * @return string
490 */
491 protected function substituteResultWithError( $e ) {
492 global $wgShowHostnames;
493
494 $result = $this->getResult();
495 // Printer may not be initialized if the extractRequestParams() fails for the main module
496 if ( !isset ( $this->mPrinter ) ) {
497 // The printer has not been created yet. Try to manually get formatter value.
498 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
499 if ( !in_array( $value, $this->mFormatNames ) ) {
500 $value = self::API_DEFAULT_FORMAT;
501 }
502
503 $this->mPrinter = $this->createPrinterByName( $value );
504 if ( $this->mPrinter->getNeedsRawData() ) {
505 $result->setRawMode();
506 }
507 }
508
509 if ( $e instanceof UsageException ) {
510 // User entered incorrect parameters - print usage screen
511 $errMessage = $e->getMessageArray();
512
513 // Only print the help message when this is for the developer, not runtime
514 if ( $this->mPrinter->getWantsHelp() || $this->mAction == 'help' ) {
515 ApiResult::setContent( $errMessage, $this->makeHelpMsg() );
516 }
517
518 } else {
519 global $wgShowSQLErrors, $wgShowExceptionDetails;
520 // Something is seriously wrong
521 if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) {
522 $info = 'Database query error';
523 } else {
524 $info = "Exception Caught: {$e->getMessage()}";
525 }
526
527 $errMessage = array(
528 'code' => 'internal_api_error_' . get_class( $e ),
529 'info' => $info,
530 );
531 ApiResult::setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' );
532 }
533
534 $result->reset();
535 $result->disableSizeCheck();
536 // Re-add the id
537 $requestid = $this->getParameter( 'requestid' );
538 if ( !is_null( $requestid ) ) {
539 $result->addValue( null, 'requestid', $requestid );
540 }
541
542 if ( $wgShowHostnames ) {
543 // servedby is especially useful when debugging errors
544 $result->addValue( null, 'servedby', wfHostName() );
545 }
546
547 $result->addValue( null, 'error', $errMessage );
548
549 return $errMessage['code'];
550 }
551
552 /**
553 * Set up for the execution.
554 * @return array
555 */
556 protected function setupExecuteAction() {
557 global $wgShowHostnames;
558
559 // First add the id to the top element
560 $result = $this->getResult();
561 $requestid = $this->getParameter( 'requestid' );
562 if ( !is_null( $requestid ) ) {
563 $result->addValue( null, 'requestid', $requestid );
564 }
565
566 if ( $wgShowHostnames ) {
567 $servedby = $this->getParameter( 'servedby' );
568 if ( $servedby ) {
569 $result->addValue( null, 'servedby', wfHostName() );
570 }
571 }
572
573 $params = $this->extractRequestParams();
574
575 $this->mShowVersions = $params['version'];
576 $this->mAction = $params['action'];
577
578 if ( !is_string( $this->mAction ) ) {
579 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
580 }
581
582 return $params;
583 }
584
585 /**
586 * Set up the module for response
587 * @return ApiBase The module that will handle this action
588 */
589 protected function setupModule() {
590 // Instantiate the module requested by the user
591 $module = new $this->mModules[$this->mAction] ( $this, $this->mAction );
592 $this->mModule = $module;
593
594 $moduleParams = $module->extractRequestParams();
595
596 // Die if token required, but not provided (unless there is a gettoken parameter)
597 $salt = $module->getTokenSalt();
598 if ( $salt !== false && !$moduleParams['gettoken'] ) {
599 if ( !isset( $moduleParams['token'] ) ) {
600 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
601 } else {
602 if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getRequest() ) ) {
603 $this->dieUsageMsg( 'sessionfailure' );
604 }
605 }
606 }
607 return $module;
608 }
609
610 /**
611 * Check the max lag if necessary
612 * @param $module ApiBase object: Api module being used
613 * @param $params Array an array containing the request parameters.
614 * @return boolean True on success, false should exit immediately
615 */
616 protected function checkMaxLag( $module, $params ) {
617 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
618 // Check for maxlag
619 global $wgShowHostnames;
620 $maxLag = $params['maxlag'];
621 list( $host, $lag ) = wfGetLB()->getMaxLag();
622 if ( $lag > $maxLag ) {
623 $response = $this->getRequest()->response();
624
625 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
626 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
627
628 if ( $wgShowHostnames ) {
629 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
630 } else {
631 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
632 }
633 return false;
634 }
635 }
636 return true;
637 }
638
639 /**
640 * Check for sufficient permissions to execute
641 * @param $module ApiBase An Api module
642 */
643 protected function checkExecutePermissions( $module ) {
644 $user = $this->getUser();
645 if ( $module->isReadMode() && !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) &&
646 !$user->isAllowed( 'read' ) )
647 {
648 $this->dieUsageMsg( 'readrequired' );
649 }
650 if ( $module->isWriteMode() ) {
651 if ( !$this->mEnableWrite ) {
652 $this->dieUsageMsg( 'writedisabled' );
653 }
654 if ( !$user->isAllowed( 'writeapi' ) ) {
655 $this->dieUsageMsg( 'writerequired' );
656 }
657 if ( wfReadOnly() ) {
658 $this->dieReadOnly();
659 }
660 }
661 }
662
663 /**
664 * Check POST for external response and setup result printer
665 * @param $module ApiBase An Api module
666 * @param $params Array an array with the request parameters
667 */
668 protected function setupExternalResponse( $module, $params ) {
669 // Ignore mustBePosted() for internal calls
670 if ( $module->mustBePosted() && !$this->getRequest()->wasPosted() ) {
671 $this->dieUsageMsg( array( 'mustbeposted', $this->mAction ) );
672 }
673
674 // See if custom printer is used
675 $this->mPrinter = $module->getCustomPrinter();
676 if ( is_null( $this->mPrinter ) ) {
677 // Create an appropriate printer
678 $this->mPrinter = $this->createPrinterByName( $params['format'] );
679 }
680
681 if ( $this->mPrinter->getNeedsRawData() ) {
682 $this->getResult()->setRawMode();
683 }
684 }
685
686 /**
687 * Execute the actual module, without any error handling
688 */
689 protected function executeAction() {
690 $params = $this->setupExecuteAction();
691 $module = $this->setupModule();
692
693 $this->checkExecutePermissions( $module );
694
695 if ( !$this->checkMaxLag( $module, $params ) ) {
696 return;
697 }
698
699 if ( !$this->mInternalMode ) {
700 $this->setupExternalResponse( $module, $params );
701 }
702
703 // Execute
704 $module->profileIn();
705 $module->execute();
706 wfRunHooks( 'APIAfterExecute', array( &$module ) );
707 $module->profileOut();
708
709 if ( !$this->mInternalMode ) {
710 // Print result data
711 $this->printResult( false );
712 }
713 }
714
715 /**
716 * Print results using the current printer
717 *
718 * @param $isError bool
719 */
720 protected function printResult( $isError ) {
721 $this->getResult()->cleanUpUTF8();
722 $printer = $this->mPrinter;
723 $printer->profileIn();
724
725 /**
726 * If the help message is requested in the default (xmlfm) format,
727 * tell the printer not to escape ampersands so that our links do
728 * not break.
729 */
730 $printer->setUnescapeAmps( ( $this->mAction == 'help' || $isError )
731 && $printer->getFormat() == 'XML' && $printer->getIsHtml() );
732
733 $printer->initPrinter( $isError );
734
735 $printer->execute();
736 $printer->closePrinter();
737 $printer->profileOut();
738 }
739
740 /**
741 * @return bool
742 */
743 public function isReadMode() {
744 return false;
745 }
746
747 /**
748 * See ApiBase for description.
749 *
750 * @return array
751 */
752 public function getAllowedParams() {
753 return array(
754 'format' => array(
755 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
756 ApiBase::PARAM_TYPE => $this->mFormatNames
757 ),
758 'action' => array(
759 ApiBase::PARAM_DFLT => 'help',
760 ApiBase::PARAM_TYPE => $this->mModuleNames
761 ),
762 'version' => false,
763 'maxlag' => array(
764 ApiBase::PARAM_TYPE => 'integer'
765 ),
766 'smaxage' => array(
767 ApiBase::PARAM_TYPE => 'integer',
768 ApiBase::PARAM_DFLT => 0
769 ),
770 'maxage' => array(
771 ApiBase::PARAM_TYPE => 'integer',
772 ApiBase::PARAM_DFLT => 0
773 ),
774 'requestid' => null,
775 'servedby' => false,
776 );
777 }
778
779 /**
780 * See ApiBase for description.
781 *
782 * @return array
783 */
784 public function getParamDescription() {
785 return array(
786 'format' => 'The format of the output',
787 'action' => 'What action you would like to perform. See below for module help',
788 'version' => 'When showing help, include version for each module',
789 'maxlag' => array(
790 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
791 'To save actions causing any more site replication lag, this parameter can make the client',
792 'wait until the replication lag is less than the specified value.',
793 'In case of a replag error, a HTTP 503 error is returned, with the message like',
794 '"Waiting for $host: $lag seconds lagged\n".',
795 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
796 ),
797 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
798 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
799 'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
800 'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error',
801 );
802 }
803
804 /**
805 * See ApiBase for description.
806 *
807 * @return array
808 */
809 public function getDescription() {
810 return array(
811 '',
812 '',
813 '**********************************************************************************************************',
814 '** **',
815 '** This is an auto-generated MediaWiki API documentation page **',
816 '** **',
817 '** Documentation and Examples: **',
818 '** https://www.mediawiki.org/wiki/API **',
819 '** **',
820 '**********************************************************************************************************',
821 '',
822 'Status: All features shown on this page should be working, but the API',
823 ' is still in active development, and may change at any time.',
824 ' Make sure to monitor our mailing list for any updates',
825 '',
826 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent',
827 ' with the key "MediaWiki-API-Error" and then both the value of the',
828 ' header and the error code sent back will be set to the same value',
829 '',
830 ' In the case of an invalid action being passed, these will have a value',
831 ' of "unknown_action"',
832 '',
833 ' For more information see https://www.mediawiki.org/wiki/API:Errors_and_warnings',
834 '',
835 'Documentation: https://www.mediawiki.org/wiki/API:Main_page',
836 'FAQ https://www.mediawiki.org/wiki/API:FAQ',
837 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
838 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
839 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
840 '',
841 '',
842 '',
843 '',
844 '',
845 );
846 }
847
848 /**
849 * @return array
850 */
851 public function getPossibleErrors() {
852 return array_merge( parent::getPossibleErrors(), array(
853 array( 'readonlytext' ),
854 array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ),
855 array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ),
856 array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ),
857 array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ),
858 ) );
859 }
860
861 /**
862 * Returns an array of strings with credits for the API
863 * @return array
864 */
865 protected function getCredits() {
866 return array(
867 'API developers:',
868 ' Roan Kattouw <Firstname>.<Lastname>@gmail.com (lead developer Sep 2007-present)',
869 ' Victor Vasiliev - vasilvv at gee mail dot com',
870 ' Bryan Tong Minh - bryan . tongminh @ gmail . com',
871 ' Sam Reed - sam @ reedyboy . net',
872 ' Yuri Astrakhan <Firstname><Lastname>@gmail.com (creator, lead developer Sep 2006-Sep 2007)',
873 '',
874 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org',
875 'or file a bug report at https://bugzilla.wikimedia.org/'
876 );
877 }
878
879 /**
880 * Sets whether the pretty-printer should format *bold* and $italics$
881 *
882 * @param $help bool
883 */
884 public function setHelp( $help = true ) {
885 $this->mPrinter->setHelp( $help );
886 }
887
888 /**
889 * Override the parent to generate help messages for all available modules.
890 *
891 * @return string
892 */
893 public function makeHelpMsg() {
894 global $wgMemc, $wgAPICacheHelpTimeout;
895 $this->setHelp();
896 // Get help text from cache if present
897 $key = wfMemcKey( 'apihelp', $this->getModuleName(),
898 SpecialVersion::getVersion( 'nodb' ) .
899 $this->getShowVersions() );
900 if ( $wgAPICacheHelpTimeout > 0 ) {
901 $cached = $wgMemc->get( $key );
902 if ( $cached ) {
903 return $cached;
904 }
905 }
906 $retval = $this->reallyMakeHelpMsg();
907 if ( $wgAPICacheHelpTimeout > 0 ) {
908 $wgMemc->set( $key, $retval, $wgAPICacheHelpTimeout );
909 }
910 return $retval;
911 }
912
913 /**
914 * @return mixed|string
915 */
916 public function reallyMakeHelpMsg() {
917 $this->setHelp();
918
919 // Use parent to make default message for the main module
920 $msg = parent::makeHelpMsg();
921
922 $astriks = str_repeat( '*** ', 14 );
923 $msg .= "\n\n$astriks Modules $astriks\n\n";
924 foreach ( array_keys( $this->mModules ) as $moduleName ) {
925 $module = new $this->mModules[$moduleName] ( $this, $moduleName );
926 $msg .= self::makeHelpMsgHeader( $module, 'action' );
927 $msg2 = $module->makeHelpMsg();
928 if ( $msg2 !== false ) {
929 $msg .= $msg2;
930 }
931 $msg .= "\n";
932 }
933
934 $msg .= "\n$astriks Permissions $astriks\n\n";
935 foreach ( self::$mRights as $right => $rightMsg ) {
936 $groups = User::getGroupsWithPermission( $right );
937 $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg[ 'msg' ], $rightMsg[ 'params' ] ) .
938 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
939
940 }
941
942 $msg .= "\n$astriks Formats $astriks\n\n";
943 foreach ( array_keys( $this->mFormats ) as $formatName ) {
944 $module = $this->createPrinterByName( $formatName );
945 $msg .= self::makeHelpMsgHeader( $module, 'format' );
946 $msg2 = $module->makeHelpMsg();
947 if ( $msg2 !== false ) {
948 $msg .= $msg2;
949 }
950 $msg .= "\n";
951 }
952
953 $msg .= "\n*** Credits: ***\n " . implode( "\n ", $this->getCredits() ) . "\n";
954
955 return $msg;
956 }
957
958 /**
959 * @param $module ApiBase
960 * @param $paramName String What type of request is this? e.g. action, query, list, prop, meta, format
961 * @return string
962 */
963 public static function makeHelpMsgHeader( $module, $paramName ) {
964 $modulePrefix = $module->getModulePrefix();
965 if ( strval( $modulePrefix ) !== '' ) {
966 $modulePrefix = "($modulePrefix) ";
967 }
968
969 return "* $paramName={$module->getModuleName()} $modulePrefix*";
970 }
971
972 private $mCanApiHighLimits = null;
973
974 /**
975 * Check whether the current user is allowed to use high limits
976 * @return bool
977 */
978 public function canApiHighLimits() {
979 if ( !isset( $this->mCanApiHighLimits ) ) {
980 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
981 }
982
983 return $this->mCanApiHighLimits;
984 }
985
986 /**
987 * Check whether the user wants us to show version information in the API help
988 * @return bool
989 */
990 public function getShowVersions() {
991 return $this->mShowVersions;
992 }
993
994 /**
995 * Returns the version information of this file, plus it includes
996 * the versions for all files that are not callable proper API modules
997 *
998 * @return array
999 */
1000 public function getVersion() {
1001 $vers = array();
1002 $vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n https://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/";
1003 $vers[] = __CLASS__ . ': $Id$';
1004 $vers[] = ApiBase::getBaseVersion();
1005 $vers[] = ApiFormatBase::getBaseVersion();
1006 $vers[] = ApiQueryBase::getBaseVersion();
1007 return $vers;
1008 }
1009
1010 /**
1011 * Add or overwrite a module in this ApiMain instance. Intended for use by extending
1012 * classes who wish to add their own modules to their lexicon or override the
1013 * behavior of inherent ones.
1014 *
1015 * @param $mdlName String The identifier for this module.
1016 * @param $mdlClass String The class where this module is implemented.
1017 */
1018 protected function addModule( $mdlName, $mdlClass ) {
1019 $this->mModules[$mdlName] = $mdlClass;
1020 }
1021
1022 /**
1023 * Add or overwrite an output format for this ApiMain. Intended for use by extending
1024 * classes who wish to add to or modify current formatters.
1025 *
1026 * @param $fmtName string The identifier for this format.
1027 * @param $fmtClass ApiFormatBase The class implementing this format.
1028 */
1029 protected function addFormat( $fmtName, $fmtClass ) {
1030 $this->mFormats[$fmtName] = $fmtClass;
1031 }
1032
1033 /**
1034 * Get the array mapping module names to class names
1035 * @return array
1036 */
1037 function getModules() {
1038 return $this->mModules;
1039 }
1040
1041 /**
1042 * Returns the list of supported formats in form ( 'format' => 'ClassName' )
1043 *
1044 * @since 1.18
1045 * @return array
1046 */
1047 public function getFormats() {
1048 return $this->mFormats;
1049 }
1050 }
1051
1052 /**
1053 * This exception will be thrown when dieUsage is called to stop module execution.
1054 * The exception handling code will print a help screen explaining how this API may be used.
1055 *
1056 * @ingroup API
1057 */
1058 class UsageException extends MWException {
1059
1060 private $mCodestr;
1061 private $mExtraData;
1062
1063 public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1064 parent::__construct( $message, $code );
1065 $this->mCodestr = $codestr;
1066 $this->mExtraData = $extradata;
1067 }
1068
1069 /**
1070 * @return string
1071 */
1072 public function getCodeString() {
1073 return $this->mCodestr;
1074 }
1075
1076 /**
1077 * @return array
1078 */
1079 public function getMessageArray() {
1080 $result = array(
1081 'code' => $this->mCodestr,
1082 'info' => $this->getMessage()
1083 );
1084 if ( is_array( $this->mExtraData ) ) {
1085 $result = array_merge( $result, $this->mExtraData );
1086 }
1087 return $result;
1088 }
1089
1090 /**
1091 * @return string
1092 */
1093 public function __toString() {
1094 return "{$this->getCodeString()}: {$this->getMessage()}";
1095 }
1096 }