MediaWiki
REL1_23
|
00001 <?php 00041 class ApiMain extends ApiBase { 00045 const API_DEFAULT_FORMAT = 'xmlfm'; 00046 00050 private static $Modules = array( 00051 'login' => 'ApiLogin', 00052 'logout' => 'ApiLogout', 00053 'createaccount' => 'ApiCreateAccount', 00054 'query' => 'ApiQuery', 00055 'expandtemplates' => 'ApiExpandTemplates', 00056 'parse' => 'ApiParse', 00057 'opensearch' => 'ApiOpenSearch', 00058 'feedcontributions' => 'ApiFeedContributions', 00059 'feedrecentchanges' => 'ApiFeedRecentChanges', 00060 'feedwatchlist' => 'ApiFeedWatchlist', 00061 'help' => 'ApiHelp', 00062 'paraminfo' => 'ApiParamInfo', 00063 'rsd' => 'ApiRsd', 00064 'compare' => 'ApiComparePages', 00065 'tokens' => 'ApiTokens', 00066 00067 // Write modules 00068 'purge' => 'ApiPurge', 00069 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp', 00070 'rollback' => 'ApiRollback', 00071 'delete' => 'ApiDelete', 00072 'undelete' => 'ApiUndelete', 00073 'protect' => 'ApiProtect', 00074 'block' => 'ApiBlock', 00075 'unblock' => 'ApiUnblock', 00076 'move' => 'ApiMove', 00077 'edit' => 'ApiEditPage', 00078 'upload' => 'ApiUpload', 00079 'filerevert' => 'ApiFileRevert', 00080 'emailuser' => 'ApiEmailUser', 00081 'watch' => 'ApiWatch', 00082 'patrol' => 'ApiPatrol', 00083 'import' => 'ApiImport', 00084 'userrights' => 'ApiUserrights', 00085 'options' => 'ApiOptions', 00086 'imagerotate' => 'ApiImageRotate', 00087 'revisiondelete' => 'ApiRevisionDelete', 00088 ); 00089 00093 private static $Formats = array( 00094 'json' => 'ApiFormatJson', 00095 'jsonfm' => 'ApiFormatJson', 00096 'php' => 'ApiFormatPhp', 00097 'phpfm' => 'ApiFormatPhp', 00098 'wddx' => 'ApiFormatWddx', 00099 'wddxfm' => 'ApiFormatWddx', 00100 'xml' => 'ApiFormatXml', 00101 'xmlfm' => 'ApiFormatXml', 00102 'yaml' => 'ApiFormatYaml', 00103 'yamlfm' => 'ApiFormatYaml', 00104 'rawfm' => 'ApiFormatJson', 00105 'txt' => 'ApiFormatTxt', 00106 'txtfm' => 'ApiFormatTxt', 00107 'dbg' => 'ApiFormatDbg', 00108 'dbgfm' => 'ApiFormatDbg', 00109 'dump' => 'ApiFormatDump', 00110 'dumpfm' => 'ApiFormatDump', 00111 'none' => 'ApiFormatNone', 00112 ); 00113 00114 // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line 00121 private static $mRights = array( 00122 'writeapi' => array( 00123 'msg' => 'Use of the write API', 00124 'params' => array() 00125 ), 00126 'apihighlimits' => array( 00127 '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.', 00128 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ) 00129 ) 00130 ); 00131 // @codingStandardsIgnoreEnd 00132 00136 private $mPrinter; 00137 00138 private $mModuleMgr, $mResult; 00139 private $mAction; 00140 private $mEnableWrite; 00141 private $mInternalMode, $mSquidMaxage, $mModule; 00142 00143 private $mCacheMode = 'private'; 00144 private $mCacheControl = array(); 00145 private $mParamsUsed = array(); 00146 00154 public function __construct( $context = null, $enableWrite = false ) { 00155 if ( $context === null ) { 00156 $context = RequestContext::getMain(); 00157 } elseif ( $context instanceof WebRequest ) { 00158 // BC for pre-1.19 00159 $request = $context; 00160 $context = RequestContext::getMain(); 00161 } 00162 // We set a derivative context so we can change stuff later 00163 $this->setContext( new DerivativeContext( $context ) ); 00164 00165 if ( isset( $request ) ) { 00166 $this->getContext()->setRequest( $request ); 00167 } 00168 00169 $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest ); 00170 00171 // Special handling for the main module: $parent === $this 00172 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' ); 00173 00174 if ( !$this->mInternalMode ) { 00175 // Impose module restrictions. 00176 // If the current user cannot read, 00177 // Remove all modules other than login 00178 global $wgUser; 00179 00180 if ( $this->getVal( 'callback' ) !== null ) { 00181 // JSON callback allows cross-site reads. 00182 // For safety, strip user credentials. 00183 wfDebug( "API: stripping user credentials for JSON callback\n" ); 00184 $wgUser = new User(); 00185 $this->getContext()->setUser( $wgUser ); 00186 } 00187 } 00188 00189 global $wgAPIModules, $wgAPIFormatModules; 00190 $this->mModuleMgr = new ApiModuleManager( $this ); 00191 $this->mModuleMgr->addModules( self::$Modules, 'action' ); 00192 $this->mModuleMgr->addModules( $wgAPIModules, 'action' ); 00193 $this->mModuleMgr->addModules( self::$Formats, 'format' ); 00194 $this->mModuleMgr->addModules( $wgAPIFormatModules, 'format' ); 00195 00196 $this->mResult = new ApiResult( $this ); 00197 $this->mEnableWrite = $enableWrite; 00198 00199 $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling() 00200 $this->mCommit = false; 00201 } 00202 00207 public function isInternalMode() { 00208 return $this->mInternalMode; 00209 } 00210 00216 public function getResult() { 00217 return $this->mResult; 00218 } 00219 00225 public function getModule() { 00226 return $this->mModule; 00227 } 00228 00234 public function getPrinter() { 00235 return $this->mPrinter; 00236 } 00237 00243 public function setCacheMaxAge( $maxage ) { 00244 $this->setCacheControl( array( 00245 'max-age' => $maxage, 00246 's-maxage' => $maxage 00247 ) ); 00248 } 00249 00275 public function setCacheMode( $mode ) { 00276 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) { 00277 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" ); 00278 00279 // Ignore for forwards-compatibility 00280 return; 00281 } 00282 00283 if ( !User::isEveryoneAllowed( 'read' ) ) { 00284 // Private wiki, only private headers 00285 if ( $mode !== 'private' ) { 00286 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" ); 00287 00288 return; 00289 } 00290 } 00291 00292 wfDebug( __METHOD__ . ": setting cache mode $mode\n" ); 00293 $this->mCacheMode = $mode; 00294 } 00295 00306 public function setCacheControl( $directives ) { 00307 $this->mCacheControl = $directives + $this->mCacheControl; 00308 } 00309 00317 public function createPrinterByName( $format ) { 00318 $printer = $this->mModuleMgr->getModule( $format, 'format' ); 00319 if ( $printer === null ) { 00320 $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' ); 00321 } 00322 00323 return $printer; 00324 } 00325 00329 public function execute() { 00330 $this->profileIn(); 00331 if ( $this->mInternalMode ) { 00332 $this->executeAction(); 00333 } else { 00334 $this->executeActionWithErrorHandling(); 00335 } 00336 00337 $this->profileOut(); 00338 } 00339 00344 protected function executeActionWithErrorHandling() { 00345 // Verify the CORS header before executing the action 00346 if ( !$this->handleCORS() ) { 00347 // handleCORS() has sent a 403, abort 00348 return; 00349 } 00350 00351 // Exit here if the request method was OPTIONS 00352 // (assume there will be a followup GET or POST) 00353 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) { 00354 return; 00355 } 00356 00357 // In case an error occurs during data output, 00358 // clear the output buffer and print just the error information 00359 ob_start(); 00360 00361 $t = microtime( true ); 00362 try { 00363 $this->executeAction(); 00364 } catch ( Exception $e ) { 00365 $this->handleException( $e ); 00366 } 00367 00368 // Log the request whether or not there was an error 00369 $this->logRequest( microtime( true ) - $t ); 00370 00371 // Send cache headers after any code which might generate an error, to 00372 // avoid sending public cache headers for errors. 00373 $this->sendCacheHeaders(); 00374 00375 if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) { 00376 echo wfReportTime(); 00377 } 00378 00379 ob_end_flush(); 00380 } 00381 00388 protected function handleException( Exception $e ) { 00389 // Bug 63145: Rollback any open database transactions 00390 if ( !( $e instanceof UsageException ) ) { 00391 // UsageExceptions are intentional, so don't rollback if that's the case 00392 MWExceptionHandler::rollbackMasterChangesAndLog( $e ); 00393 } 00394 00395 // Allow extra cleanup and logging 00396 wfRunHooks( 'ApiMain::onException', array( $this, $e ) ); 00397 00398 // Log it 00399 if ( !( $e instanceof UsageException ) ) { 00400 MWExceptionHandler::logException( $e ); 00401 } 00402 00403 // Handle any kind of exception by outputting properly formatted error message. 00404 // If this fails, an unhandled exception should be thrown so that global error 00405 // handler will process and log it. 00406 00407 $errCode = $this->substituteResultWithError( $e ); 00408 00409 // Error results should not be cached 00410 $this->setCacheMode( 'private' ); 00411 00412 $response = $this->getRequest()->response(); 00413 $headerStr = 'MediaWiki-API-Error: ' . $errCode; 00414 if ( $e->getCode() === 0 ) { 00415 $response->header( $headerStr ); 00416 } else { 00417 $response->header( $headerStr, true, $e->getCode() ); 00418 } 00419 00420 // Reset and print just the error message 00421 ob_clean(); 00422 00423 // If the error occurred during printing, do a printer->profileOut() 00424 $this->mPrinter->safeProfileOut(); 00425 $this->printResult( true ); 00426 } 00427 00437 public static function handleApiBeforeMainException( Exception $e ) { 00438 ob_start(); 00439 00440 try { 00441 $main = new self( RequestContext::getMain(), false ); 00442 $main->handleException( $e ); 00443 } catch ( Exception $e2 ) { 00444 // Nope, even that didn't work. Punt. 00445 throw $e; 00446 } 00447 00448 // Log the request and reset cache headers 00449 $main->logRequest( 0 ); 00450 $main->sendCacheHeaders(); 00451 00452 ob_end_flush(); 00453 } 00454 00467 protected function handleCORS() { 00468 global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions; 00469 00470 $originParam = $this->getParameter( 'origin' ); // defaults to null 00471 if ( $originParam === null ) { 00472 // No origin parameter, nothing to do 00473 return true; 00474 } 00475 00476 $request = $this->getRequest(); 00477 $response = $request->response(); 00478 // Origin: header is a space-separated list of origins, check all of them 00479 $originHeader = $request->getHeader( 'Origin' ); 00480 if ( $originHeader === false ) { 00481 $origins = array(); 00482 } else { 00483 $origins = explode( ' ', $originHeader ); 00484 } 00485 00486 if ( !in_array( $originParam, $origins ) ) { 00487 // origin parameter set but incorrect 00488 // Send a 403 response 00489 $message = HttpStatus::getMessage( 403 ); 00490 $response->header( "HTTP/1.1 403 $message", true, 403 ); 00491 $response->header( 'Cache-Control: no-cache' ); 00492 echo "'origin' parameter does not match Origin header\n"; 00493 00494 return false; 00495 } 00496 00497 $matchOrigin = self::matchOrigin( 00498 $originParam, 00499 $wgCrossSiteAJAXdomains, 00500 $wgCrossSiteAJAXdomainExceptions 00501 ); 00502 00503 if ( $matchOrigin ) { 00504 $response->header( "Access-Control-Allow-Origin: $originParam" ); 00505 $response->header( 'Access-Control-Allow-Credentials: true' ); 00506 $this->getOutput()->addVaryHeader( 'Origin' ); 00507 } 00508 00509 return true; 00510 } 00511 00520 protected static function matchOrigin( $value, $rules, $exceptions ) { 00521 foreach ( $rules as $rule ) { 00522 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) { 00523 // Rule matches, check exceptions 00524 foreach ( $exceptions as $exc ) { 00525 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) { 00526 return false; 00527 } 00528 } 00529 00530 return true; 00531 } 00532 } 00533 00534 return false; 00535 } 00536 00545 protected static function wildcardToRegex( $wildcard ) { 00546 $wildcard = preg_quote( $wildcard, '/' ); 00547 $wildcard = str_replace( 00548 array( '\*', '\?' ), 00549 array( '.*?', '.' ), 00550 $wildcard 00551 ); 00552 00553 return "/https?:\/\/$wildcard/"; 00554 } 00555 00556 protected function sendCacheHeaders() { 00557 global $wgUseXVO, $wgVaryOnXFP; 00558 $response = $this->getRequest()->response(); 00559 $out = $this->getOutput(); 00560 00561 if ( $wgVaryOnXFP ) { 00562 $out->addVaryHeader( 'X-Forwarded-Proto' ); 00563 } 00564 00565 if ( $this->mCacheMode == 'private' ) { 00566 $response->header( 'Cache-Control: private' ); 00567 00568 return; 00569 } 00570 00571 if ( $this->mCacheMode == 'anon-public-user-private' ) { 00572 $out->addVaryHeader( 'Cookie' ); 00573 $response->header( $out->getVaryHeader() ); 00574 if ( $wgUseXVO ) { 00575 $response->header( $out->getXVO() ); 00576 if ( $out->haveCacheVaryCookies() ) { 00577 // Logged in, mark this request private 00578 $response->header( 'Cache-Control: private' ); 00579 00580 return; 00581 } 00582 // Logged out, send normal public headers below 00583 } elseif ( session_id() != '' ) { 00584 // Logged in or otherwise has session (e.g. anonymous users who have edited) 00585 // Mark request private 00586 $response->header( 'Cache-Control: private' ); 00587 00588 return; 00589 } // else no XVO and anonymous, send public headers below 00590 } 00591 00592 // Send public headers 00593 $response->header( $out->getVaryHeader() ); 00594 if ( $wgUseXVO ) { 00595 $response->header( $out->getXVO() ); 00596 } 00597 00598 // If nobody called setCacheMaxAge(), use the (s)maxage parameters 00599 if ( !isset( $this->mCacheControl['s-maxage'] ) ) { 00600 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' ); 00601 } 00602 if ( !isset( $this->mCacheControl['max-age'] ) ) { 00603 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' ); 00604 } 00605 00606 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) { 00607 // Public cache not requested 00608 // Sending a Vary header in this case is harmless, and protects us 00609 // against conditional calls of setCacheMaxAge(). 00610 $response->header( 'Cache-Control: private' ); 00611 00612 return; 00613 } 00614 00615 $this->mCacheControl['public'] = true; 00616 00617 // Send an Expires header 00618 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] ); 00619 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge ); 00620 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) ); 00621 00622 // Construct the Cache-Control header 00623 $ccHeader = ''; 00624 $separator = ''; 00625 foreach ( $this->mCacheControl as $name => $value ) { 00626 if ( is_bool( $value ) ) { 00627 if ( $value ) { 00628 $ccHeader .= $separator . $name; 00629 $separator = ', '; 00630 } 00631 } else { 00632 $ccHeader .= $separator . "$name=$value"; 00633 $separator = ', '; 00634 } 00635 } 00636 00637 $response->header( "Cache-Control: $ccHeader" ); 00638 } 00639 00646 protected function substituteResultWithError( $e ) { 00647 global $wgShowHostnames; 00648 00649 $result = $this->getResult(); 00650 00651 // Printer may not be initialized if the extractRequestParams() fails for the main module 00652 if ( !isset( $this->mPrinter ) ) { 00653 // The printer has not been created yet. Try to manually get formatter value. 00654 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT ); 00655 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) { 00656 $value = self::API_DEFAULT_FORMAT; 00657 } 00658 00659 $this->mPrinter = $this->createPrinterByName( $value ); 00660 } 00661 00662 // Printer may not be able to handle errors. This is particularly 00663 // likely if the module returns something for getCustomPrinter(). 00664 if ( !$this->mPrinter->canPrintErrors() ) { 00665 $this->mPrinter->safeProfileOut(); 00666 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT ); 00667 } 00668 00669 // Update raw mode flag for the selected printer. 00670 $result->setRawMode( $this->mPrinter->getNeedsRawData() ); 00671 00672 if ( $e instanceof UsageException ) { 00673 // User entered incorrect parameters - print usage screen 00674 $errMessage = $e->getMessageArray(); 00675 00676 // Only print the help message when this is for the developer, not runtime 00677 if ( $this->mPrinter->getWantsHelp() || $this->mAction == 'help' ) { 00678 ApiResult::setContent( $errMessage, $this->makeHelpMsg() ); 00679 } 00680 } else { 00681 global $wgShowSQLErrors, $wgShowExceptionDetails; 00682 // Something is seriously wrong 00683 if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) { 00684 $info = 'Database query error'; 00685 } else { 00686 $info = "Exception Caught: {$e->getMessage()}"; 00687 } 00688 00689 $errMessage = array( 00690 'code' => 'internal_api_error_' . get_class( $e ), 00691 'info' => $info, 00692 ); 00693 ApiResult::setContent( 00694 $errMessage, 00695 $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' 00696 ); 00697 } 00698 00699 // Remember all the warnings to re-add them later 00700 $oldResult = $result->getData(); 00701 $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null; 00702 00703 $result->reset(); 00704 $result->disableSizeCheck(); 00705 // Re-add the id 00706 $requestid = $this->getParameter( 'requestid' ); 00707 if ( !is_null( $requestid ) ) { 00708 $result->addValue( null, 'requestid', $requestid ); 00709 } 00710 if ( $wgShowHostnames ) { 00711 // servedby is especially useful when debugging errors 00712 $result->addValue( null, 'servedby', wfHostName() ); 00713 } 00714 if ( $warnings !== null ) { 00715 $result->addValue( null, 'warnings', $warnings ); 00716 } 00717 00718 $result->addValue( null, 'error', $errMessage ); 00719 00720 return $errMessage['code']; 00721 } 00722 00727 protected function setupExecuteAction() { 00728 global $wgShowHostnames; 00729 00730 // First add the id to the top element 00731 $result = $this->getResult(); 00732 $requestid = $this->getParameter( 'requestid' ); 00733 if ( !is_null( $requestid ) ) { 00734 $result->addValue( null, 'requestid', $requestid ); 00735 } 00736 00737 if ( $wgShowHostnames ) { 00738 $servedby = $this->getParameter( 'servedby' ); 00739 if ( $servedby ) { 00740 $result->addValue( null, 'servedby', wfHostName() ); 00741 } 00742 } 00743 00744 $params = $this->extractRequestParams(); 00745 00746 $this->mAction = $params['action']; 00747 00748 if ( !is_string( $this->mAction ) ) { 00749 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); 00750 } 00751 00752 return $params; 00753 } 00754 00759 protected function setupModule() { 00760 // Instantiate the module requested by the user 00761 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' ); 00762 if ( $module === null ) { 00763 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); 00764 } 00765 $moduleParams = $module->extractRequestParams(); 00766 00767 // Die if token required, but not provided 00768 $salt = $module->getTokenSalt(); 00769 if ( $salt !== false ) { 00770 if ( !isset( $moduleParams['token'] ) ) { 00771 $this->dieUsageMsg( array( 'missingparam', 'token' ) ); 00772 } 00773 00774 if ( !$this->getUser()->matchEditToken( 00775 $moduleParams['token'], 00776 $salt, 00777 $this->getContext()->getRequest() ) 00778 ) { 00779 $this->dieUsageMsg( 'sessionfailure' ); 00780 } 00781 } 00782 00783 return $module; 00784 } 00785 00792 protected function checkMaxLag( $module, $params ) { 00793 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) { 00794 // Check for maxlag 00795 global $wgShowHostnames; 00796 $maxLag = $params['maxlag']; 00797 list( $host, $lag ) = wfGetLB()->getMaxLag(); 00798 if ( $lag > $maxLag ) { 00799 $response = $this->getRequest()->response(); 00800 00801 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); 00802 $response->header( 'X-Database-Lag: ' . intval( $lag ) ); 00803 00804 if ( $wgShowHostnames ) { 00805 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); 00806 } 00807 00808 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); 00809 } 00810 } 00811 00812 return true; 00813 } 00814 00819 protected function checkExecutePermissions( $module ) { 00820 $user = $this->getUser(); 00821 if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) && 00822 !$user->isAllowed( 'read' ) 00823 ) { 00824 $this->dieUsageMsg( 'readrequired' ); 00825 } 00826 if ( $module->isWriteMode() ) { 00827 if ( !$this->mEnableWrite ) { 00828 $this->dieUsageMsg( 'writedisabled' ); 00829 } 00830 if ( !$user->isAllowed( 'writeapi' ) ) { 00831 $this->dieUsageMsg( 'writerequired' ); 00832 } 00833 if ( wfReadOnly() ) { 00834 $this->dieReadOnly(); 00835 } 00836 } 00837 00838 // Allow extensions to stop execution for arbitrary reasons. 00839 $message = false; 00840 if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) { 00841 $this->dieUsageMsg( $message ); 00842 } 00843 } 00844 00849 protected function checkAsserts( $params ) { 00850 if ( isset( $params['assert'] ) ) { 00851 $user = $this->getUser(); 00852 switch ( $params['assert'] ) { 00853 case 'user': 00854 if ( $user->isAnon() ) { 00855 $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' ); 00856 } 00857 break; 00858 case 'bot': 00859 if ( !$user->isAllowed( 'bot' ) ) { 00860 $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' ); 00861 } 00862 break; 00863 } 00864 } 00865 } 00866 00872 protected function setupExternalResponse( $module, $params ) { 00873 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) { 00874 // Module requires POST. GET request might still be allowed 00875 // if $wgDebugApi is true, otherwise fail. 00876 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) ); 00877 } 00878 00879 // See if custom printer is used 00880 $this->mPrinter = $module->getCustomPrinter(); 00881 if ( is_null( $this->mPrinter ) ) { 00882 // Create an appropriate printer 00883 $this->mPrinter = $this->createPrinterByName( $params['format'] ); 00884 } 00885 00886 if ( $this->mPrinter->getNeedsRawData() ) { 00887 $this->getResult()->setRawMode(); 00888 } 00889 } 00890 00894 protected function executeAction() { 00895 $params = $this->setupExecuteAction(); 00896 $module = $this->setupModule(); 00897 $this->mModule = $module; 00898 00899 $this->checkExecutePermissions( $module ); 00900 00901 if ( !$this->checkMaxLag( $module, $params ) ) { 00902 return; 00903 } 00904 00905 if ( !$this->mInternalMode ) { 00906 $this->setupExternalResponse( $module, $params ); 00907 } 00908 00909 $this->checkAsserts( $params ); 00910 00911 // Execute 00912 $module->profileIn(); 00913 $module->execute(); 00914 wfRunHooks( 'APIAfterExecute', array( &$module ) ); 00915 $module->profileOut(); 00916 00917 $this->reportUnusedParams(); 00918 00919 if ( !$this->mInternalMode ) { 00920 //append Debug information 00921 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() ); 00922 00923 // Print result data 00924 $this->printResult( false ); 00925 } 00926 } 00927 00932 protected function logRequest( $time ) { 00933 $request = $this->getRequest(); 00934 $milliseconds = $time === null ? '?' : round( $time * 1000 ); 00935 $s = 'API' . 00936 ' ' . $request->getMethod() . 00937 ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . 00938 ' ' . $request->getIP() . 00939 ' T=' . $milliseconds . 'ms'; 00940 foreach ( $this->getParamsUsed() as $name ) { 00941 $value = $request->getVal( $name ); 00942 if ( $value === null ) { 00943 continue; 00944 } 00945 $s .= ' ' . $name . '='; 00946 if ( strlen( $value ) > 256 ) { 00947 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) ); 00948 $s .= $encValue . '[...]'; 00949 } else { 00950 $s .= $this->encodeRequestLogValue( $value ); 00951 } 00952 } 00953 $s .= "\n"; 00954 wfDebugLog( 'api', $s, 'private' ); 00955 } 00956 00960 protected function encodeRequestLogValue( $s ) { 00961 static $table; 00962 if ( !$table ) { 00963 $chars = ';@$!*(),/:'; 00964 $numChars = strlen( $chars ); 00965 for ( $i = 0; $i < $numChars; $i++ ) { 00966 $table[rawurlencode( $chars[$i] )] = $chars[$i]; 00967 } 00968 } 00969 00970 return strtr( rawurlencode( $s ), $table ); 00971 } 00972 00976 protected function getParamsUsed() { 00977 return array_keys( $this->mParamsUsed ); 00978 } 00979 00983 public function getVal( $name, $default = null ) { 00984 $this->mParamsUsed[$name] = true; 00985 00986 return $this->getRequest()->getVal( $name, $default ); 00987 } 00988 00993 public function getCheck( $name ) { 00994 $this->mParamsUsed[$name] = true; 00995 00996 return $this->getRequest()->getCheck( $name ); 00997 } 00998 01006 public function getUpload( $name ) { 01007 $this->mParamsUsed[$name] = true; 01008 01009 return $this->getRequest()->getUpload( $name ); 01010 } 01011 01016 protected function reportUnusedParams() { 01017 $paramsUsed = $this->getParamsUsed(); 01018 $allParams = $this->getRequest()->getValueNames(); 01019 01020 if ( !$this->mInternalMode ) { 01021 // Printer has not yet executed; don't warn that its parameters are unused 01022 $printerParams = array_map( 01023 array( $this->mPrinter, 'encodeParamName' ), 01024 array_keys( $this->mPrinter->getFinalParams() ?: array() ) 01025 ); 01026 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams ); 01027 } else { 01028 $unusedParams = array_diff( $allParams, $paramsUsed ); 01029 } 01030 01031 if ( count( $unusedParams ) ) { 01032 $s = count( $unusedParams ) > 1 ? 's' : ''; 01033 $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" ); 01034 } 01035 } 01036 01042 protected function printResult( $isError ) { 01043 global $wgDebugAPI; 01044 if ( $wgDebugAPI !== false ) { 01045 $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' ); 01046 } 01047 01048 $this->getResult()->cleanUpUTF8(); 01049 $printer = $this->mPrinter; 01050 $printer->profileIn(); 01051 01057 $isHelp = $isError || $this->mAction == 'help'; 01058 $printer->setUnescapeAmps( $isHelp && $printer->getFormat() == 'XML' && $printer->getIsHtml() ); 01059 01060 $printer->initPrinter( $isHelp ); 01061 01062 $printer->execute(); 01063 $printer->closePrinter(); 01064 $printer->profileOut(); 01065 } 01066 01070 public function isReadMode() { 01071 return false; 01072 } 01073 01079 public function getAllowedParams() { 01080 return array( 01081 'format' => array( 01082 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT, 01083 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' ) 01084 ), 01085 'action' => array( 01086 ApiBase::PARAM_DFLT => 'help', 01087 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' ) 01088 ), 01089 'maxlag' => array( 01090 ApiBase::PARAM_TYPE => 'integer' 01091 ), 01092 'smaxage' => array( 01093 ApiBase::PARAM_TYPE => 'integer', 01094 ApiBase::PARAM_DFLT => 0 01095 ), 01096 'maxage' => array( 01097 ApiBase::PARAM_TYPE => 'integer', 01098 ApiBase::PARAM_DFLT => 0 01099 ), 01100 'assert' => array( 01101 ApiBase::PARAM_TYPE => array( 'user', 'bot' ) 01102 ), 01103 'requestid' => null, 01104 'servedby' => false, 01105 'origin' => null, 01106 ); 01107 } 01108 01114 public function getParamDescription() { 01115 return array( 01116 'format' => 'The format of the output', 01117 'action' => 'What action you would like to perform. See below for module help', 01118 'maxlag' => array( 01119 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.', 01120 'To save actions causing any more site replication lag, this parameter can make the client', 01121 'wait until the replication lag is less than the specified value.', 01122 'In case of a replag error, error code "maxlag" is returned, with the message like', 01123 '"Waiting for $host: $lag seconds lagged\n".', 01124 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information', 01125 ), 01126 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached', 01127 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', 01128 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"', 01129 'requestid' => 'Request ID to distinguish requests. This will just be output back to you', 01130 'servedby' => 'Include the hostname that served the request in the ' . 01131 'results. Unconditionally shown on error', 01132 'origin' => array( 01133 'When accessing the API using a cross-domain AJAX request (CORS), set this to the', 01134 'originating domain. This must be included in any pre-flight request, and', 01135 'therefore must be part of the request URI (not the POST body). This must match', 01136 'one of the origins in the Origin: header exactly, so it has to be set to ', 01137 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this', 01138 'parameter does not match the Origin: header, a 403 response will be returned. If', 01139 'this parameter matches the Origin: header and the origin is whitelisted, an', 01140 'Access-Control-Allow-Origin header will be set.', 01141 ), 01142 ); 01143 } 01144 01150 public function getDescription() { 01151 return array( 01152 '', 01153 '', 01154 '**********************************************************************************************', 01155 '** **', 01156 '** This is an auto-generated MediaWiki API documentation page **', 01157 '** **', 01158 '** Documentation and Examples: **', 01159 '** https://www.mediawiki.org/wiki/API **', 01160 '** **', 01161 '**********************************************************************************************', 01162 '', 01163 'Status: All features shown on this page should be working, but the API', 01164 ' is still in active development, and may change at any time.', 01165 ' Make sure to monitor our mailing list for any updates.', 01166 '', 01167 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent', 01168 ' with the key "MediaWiki-API-Error" and then both the value of the', 01169 ' header and the error code sent back will be set to the same value.', 01170 '', 01171 ' In the case of an invalid action being passed, these will have a value', 01172 ' of "unknown_action".', 01173 '', 01174 ' For more information see https://www.mediawiki.org' . 01175 '/wiki/API:Errors_and_warnings', 01176 '', 01177 'Documentation: https://www.mediawiki.org/wiki/API:Main_page', 01178 'FAQ https://www.mediawiki.org/wiki/API:FAQ', 01179 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api', 01180 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce', 01181 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' . 01182 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts', 01183 '', 01184 '', 01185 '', 01186 '', 01187 '', 01188 ); 01189 } 01190 01194 public function getPossibleErrors() { 01195 return array_merge( parent::getPossibleErrors(), array( 01196 array( 'readonlytext' ), 01197 array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ), 01198 array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ), 01199 array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ), 01200 array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ), 01201 array( 'code' => 'assertuserfailed', 'info' => 'Assertion that the user is logged in failed' ), 01202 array( 01203 'code' => 'assertbotfailed', 01204 'info' => 'Assertion that the user has the bot right failed' 01205 ), 01206 ) ); 01207 } 01208 01213 protected function getCredits() { 01214 return array( 01215 'API developers:', 01216 ' Roan Kattouw (lead developer Sep 2007-2009)', 01217 ' Victor Vasiliev', 01218 ' Bryan Tong Minh', 01219 ' Sam Reed', 01220 ' Yuri Astrakhan (creator, lead developer Sep 2006-Sep 2007, 2012-present)', 01221 '', 01222 'Please send your comments, suggestions and questions to [email protected]', 01223 'or file a bug report at https://bugzilla.wikimedia.org/' 01224 ); 01225 } 01226 01232 public function setHelp( $help = true ) { 01233 $this->mPrinter->setHelp( $help ); 01234 } 01235 01241 public function makeHelpMsg() { 01242 global $wgMemc, $wgAPICacheHelpTimeout; 01243 $this->setHelp(); 01244 // Get help text from cache if present 01245 $key = wfMemcKey( 'apihelp', $this->getModuleName(), 01246 str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) ); 01247 if ( $wgAPICacheHelpTimeout > 0 ) { 01248 $cached = $wgMemc->get( $key ); 01249 if ( $cached ) { 01250 return $cached; 01251 } 01252 } 01253 $retval = $this->reallyMakeHelpMsg(); 01254 if ( $wgAPICacheHelpTimeout > 0 ) { 01255 $wgMemc->set( $key, $retval, $wgAPICacheHelpTimeout ); 01256 } 01257 01258 return $retval; 01259 } 01260 01264 public function reallyMakeHelpMsg() { 01265 $this->setHelp(); 01266 01267 // Use parent to make default message for the main module 01268 $msg = parent::makeHelpMsg(); 01269 01270 $astriks = str_repeat( '*** ', 14 ); 01271 $msg .= "\n\n$astriks Modules $astriks\n\n"; 01272 01273 foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) { 01274 $module = $this->mModuleMgr->getModule( $name ); 01275 $msg .= self::makeHelpMsgHeader( $module, 'action' ); 01276 01277 $msg2 = $module->makeHelpMsg(); 01278 if ( $msg2 !== false ) { 01279 $msg .= $msg2; 01280 } 01281 $msg .= "\n"; 01282 } 01283 01284 $msg .= "\n$astriks Permissions $astriks\n\n"; 01285 foreach ( self::$mRights as $right => $rightMsg ) { 01286 $groups = User::getGroupsWithPermission( $right ); 01287 $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg['msg'], $rightMsg['params'] ) . 01288 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n"; 01289 } 01290 01291 $msg .= "\n$astriks Formats $astriks\n\n"; 01292 foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) { 01293 $module = $this->mModuleMgr->getModule( $name ); 01294 $msg .= self::makeHelpMsgHeader( $module, 'format' ); 01295 $msg2 = $module->makeHelpMsg(); 01296 if ( $msg2 !== false ) { 01297 $msg .= $msg2; 01298 } 01299 $msg .= "\n"; 01300 } 01301 01302 $msg .= "\n*** Credits: ***\n " . implode( "\n ", $this->getCredits() ) . "\n"; 01303 01304 return $msg; 01305 } 01306 01313 public static function makeHelpMsgHeader( $module, $paramName ) { 01314 $modulePrefix = $module->getModulePrefix(); 01315 if ( strval( $modulePrefix ) !== '' ) { 01316 $modulePrefix = "($modulePrefix) "; 01317 } 01318 01319 return "* $paramName={$module->getModuleName()} $modulePrefix*"; 01320 } 01321 01322 private $mCanApiHighLimits = null; 01323 01328 public function canApiHighLimits() { 01329 if ( !isset( $this->mCanApiHighLimits ) ) { 01330 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' ); 01331 } 01332 01333 return $this->mCanApiHighLimits; 01334 } 01335 01341 public function getShowVersions() { 01342 wfDeprecated( __METHOD__, '1.21' ); 01343 01344 return false; 01345 } 01346 01351 public function getModuleManager() { 01352 return $this->mModuleMgr; 01353 } 01354 01364 protected function addModule( $name, $class ) { 01365 $this->getModuleManager()->addModule( $name, 'action', $class ); 01366 } 01367 01376 protected function addFormat( $name, $class ) { 01377 $this->getModuleManager()->addModule( $name, 'format', $class ); 01378 } 01379 01385 function getModules() { 01386 return $this->getModuleManager()->getNamesWithClasses( 'action' ); 01387 } 01388 01396 public function getFormats() { 01397 return $this->getModuleManager()->getNamesWithClasses( 'format' ); 01398 } 01399 } 01400 01407 class UsageException extends MWException { 01408 01409 private $mCodestr; 01410 01414 private $mExtraData; 01415 01422 public function __construct( $message, $codestr, $code = 0, $extradata = null ) { 01423 parent::__construct( $message, $code ); 01424 $this->mCodestr = $codestr; 01425 $this->mExtraData = $extradata; 01426 } 01427 01431 public function getCodeString() { 01432 return $this->mCodestr; 01433 } 01434 01438 public function getMessageArray() { 01439 $result = array( 01440 'code' => $this->mCodestr, 01441 'info' => $this->getMessage() 01442 ); 01443 if ( is_array( $this->mExtraData ) ) { 01444 $result = array_merge( $result, $this->mExtraData ); 01445 } 01446 01447 return $result; 01448 } 01449 01453 public function __toString() { 01454 return "{$this->getCodeString()}: {$this->getMessage()}"; 01455 } 01456 }