MediaWiki
REL1_24
|
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 'clearhasmsg' => 'ApiClearHasMsg', 00085 'userrights' => 'ApiUserrights', 00086 'options' => 'ApiOptions', 00087 'imagerotate' => 'ApiImageRotate', 00088 'revisiondelete' => 'ApiRevisionDelete', 00089 ); 00090 00094 private static $Formats = array( 00095 'json' => 'ApiFormatJson', 00096 'jsonfm' => 'ApiFormatJson', 00097 'php' => 'ApiFormatPhp', 00098 'phpfm' => 'ApiFormatPhp', 00099 'wddx' => 'ApiFormatWddx', 00100 'wddxfm' => 'ApiFormatWddx', 00101 'xml' => 'ApiFormatXml', 00102 'xmlfm' => 'ApiFormatXml', 00103 'yaml' => 'ApiFormatYaml', 00104 'yamlfm' => 'ApiFormatYaml', 00105 'rawfm' => 'ApiFormatJson', 00106 'txt' => 'ApiFormatTxt', 00107 'txtfm' => 'ApiFormatTxt', 00108 'dbg' => 'ApiFormatDbg', 00109 'dbgfm' => 'ApiFormatDbg', 00110 'dump' => 'ApiFormatDump', 00111 'dumpfm' => 'ApiFormatDump', 00112 'none' => 'ApiFormatNone', 00113 ); 00114 00115 // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line 00122 private static $mRights = array( 00123 'writeapi' => array( 00124 'msg' => 'Use of the write API', 00125 'params' => array() 00126 ), 00127 'apihighlimits' => array( 00128 '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.', 00129 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ) 00130 ) 00131 ); 00132 // @codingStandardsIgnoreEnd 00133 00137 private $mPrinter; 00138 00139 private $mModuleMgr, $mResult; 00140 private $mAction; 00141 private $mEnableWrite; 00142 private $mInternalMode, $mSquidMaxage, $mModule; 00143 00144 private $mCacheMode = 'private'; 00145 private $mCacheControl = array(); 00146 private $mParamsUsed = array(); 00147 00155 public function __construct( $context = null, $enableWrite = false ) { 00156 if ( $context === null ) { 00157 $context = RequestContext::getMain(); 00158 } elseif ( $context instanceof WebRequest ) { 00159 // BC for pre-1.19 00160 $request = $context; 00161 $context = RequestContext::getMain(); 00162 } 00163 // We set a derivative context so we can change stuff later 00164 $this->setContext( new DerivativeContext( $context ) ); 00165 00166 if ( isset( $request ) ) { 00167 $this->getContext()->setRequest( $request ); 00168 } 00169 00170 $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest ); 00171 00172 // Special handling for the main module: $parent === $this 00173 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' ); 00174 00175 if ( !$this->mInternalMode ) { 00176 // Impose module restrictions. 00177 // If the current user cannot read, 00178 // Remove all modules other than login 00179 global $wgUser; 00180 00181 if ( $this->getVal( 'callback' ) !== null ) { 00182 // JSON callback allows cross-site reads. 00183 // For safety, strip user credentials. 00184 wfDebug( "API: stripping user credentials for JSON callback\n" ); 00185 $wgUser = new User(); 00186 $this->getContext()->setUser( $wgUser ); 00187 } 00188 } 00189 00190 $config = $this->getConfig(); 00191 $this->mModuleMgr = new ApiModuleManager( $this ); 00192 $this->mModuleMgr->addModules( self::$Modules, 'action' ); 00193 $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' ); 00194 $this->mModuleMgr->addModules( self::$Formats, 'format' ); 00195 $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' ); 00196 00197 $this->mResult = new ApiResult( $this ); 00198 $this->mEnableWrite = $enableWrite; 00199 00200 $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling() 00201 $this->mCommit = false; 00202 } 00203 00208 public function isInternalMode() { 00209 return $this->mInternalMode; 00210 } 00211 00217 public function getResult() { 00218 return $this->mResult; 00219 } 00220 00226 public function getModule() { 00227 return $this->mModule; 00228 } 00229 00235 public function getPrinter() { 00236 return $this->mPrinter; 00237 } 00238 00244 public function setCacheMaxAge( $maxage ) { 00245 $this->setCacheControl( array( 00246 'max-age' => $maxage, 00247 's-maxage' => $maxage 00248 ) ); 00249 } 00250 00276 public function setCacheMode( $mode ) { 00277 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) { 00278 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" ); 00279 00280 // Ignore for forwards-compatibility 00281 return; 00282 } 00283 00284 if ( !User::isEveryoneAllowed( 'read' ) ) { 00285 // Private wiki, only private headers 00286 if ( $mode !== 'private' ) { 00287 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" ); 00288 00289 return; 00290 } 00291 } 00292 00293 wfDebug( __METHOD__ . ": setting cache mode $mode\n" ); 00294 $this->mCacheMode = $mode; 00295 } 00296 00307 public function setCacheControl( $directives ) { 00308 $this->mCacheControl = $directives + $this->mCacheControl; 00309 } 00310 00318 public function createPrinterByName( $format ) { 00319 $printer = $this->mModuleMgr->getModule( $format, 'format' ); 00320 if ( $printer === null ) { 00321 $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' ); 00322 } 00323 00324 return $printer; 00325 } 00326 00330 public function execute() { 00331 $this->profileIn(); 00332 if ( $this->mInternalMode ) { 00333 $this->executeAction(); 00334 } else { 00335 $this->executeActionWithErrorHandling(); 00336 } 00337 00338 $this->profileOut(); 00339 } 00340 00345 protected function executeActionWithErrorHandling() { 00346 // Verify the CORS header before executing the action 00347 if ( !$this->handleCORS() ) { 00348 // handleCORS() has sent a 403, abort 00349 return; 00350 } 00351 00352 // Exit here if the request method was OPTIONS 00353 // (assume there will be a followup GET or POST) 00354 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) { 00355 return; 00356 } 00357 00358 // In case an error occurs during data output, 00359 // clear the output buffer and print just the error information 00360 ob_start(); 00361 00362 $t = microtime( true ); 00363 try { 00364 $this->executeAction(); 00365 } catch ( Exception $e ) { 00366 $this->handleException( $e ); 00367 } 00368 00369 // Log the request whether or not there was an error 00370 $this->logRequest( microtime( true ) - $t ); 00371 00372 // Send cache headers after any code which might generate an error, to 00373 // avoid sending public cache headers for errors. 00374 $this->sendCacheHeaders(); 00375 00376 if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) { 00377 echo wfReportTime(); 00378 } 00379 00380 ob_end_flush(); 00381 } 00382 00389 protected function handleException( Exception $e ) { 00390 // Bug 63145: Rollback any open database transactions 00391 if ( !( $e instanceof UsageException ) ) { 00392 // UsageExceptions are intentional, so don't rollback if that's the case 00393 MWExceptionHandler::rollbackMasterChangesAndLog( $e ); 00394 } 00395 00396 // Allow extra cleanup and logging 00397 wfRunHooks( 'ApiMain::onException', array( $this, $e ) ); 00398 00399 // Log it 00400 if ( !( $e instanceof UsageException ) ) { 00401 MWExceptionHandler::logException( $e ); 00402 } 00403 00404 // Handle any kind of exception by outputting properly formatted error message. 00405 // If this fails, an unhandled exception should be thrown so that global error 00406 // handler will process and log it. 00407 00408 $errCode = $this->substituteResultWithError( $e ); 00409 00410 // Error results should not be cached 00411 $this->setCacheMode( 'private' ); 00412 00413 $response = $this->getRequest()->response(); 00414 $headerStr = 'MediaWiki-API-Error: ' . $errCode; 00415 if ( $e->getCode() === 0 ) { 00416 $response->header( $headerStr ); 00417 } else { 00418 $response->header( $headerStr, true, $e->getCode() ); 00419 } 00420 00421 // Reset and print just the error message 00422 ob_clean(); 00423 00424 // If the error occurred during printing, do a printer->profileOut() 00425 $this->mPrinter->safeProfileOut(); 00426 $this->printResult( true ); 00427 } 00428 00438 public static function handleApiBeforeMainException( Exception $e ) { 00439 ob_start(); 00440 00441 try { 00442 $main = new self( RequestContext::getMain(), false ); 00443 $main->handleException( $e ); 00444 } catch ( Exception $e2 ) { 00445 // Nope, even that didn't work. Punt. 00446 throw $e; 00447 } 00448 00449 // Log the request and reset cache headers 00450 $main->logRequest( 0 ); 00451 $main->sendCacheHeaders(); 00452 00453 ob_end_flush(); 00454 } 00455 00468 protected function handleCORS() { 00469 $originParam = $this->getParameter( 'origin' ); // defaults to null 00470 if ( $originParam === null ) { 00471 // No origin parameter, nothing to do 00472 return true; 00473 } 00474 00475 $request = $this->getRequest(); 00476 $response = $request->response(); 00477 // Origin: header is a space-separated list of origins, check all of them 00478 $originHeader = $request->getHeader( 'Origin' ); 00479 if ( $originHeader === false ) { 00480 $origins = array(); 00481 } else { 00482 $origins = explode( ' ', $originHeader ); 00483 } 00484 00485 if ( !in_array( $originParam, $origins ) ) { 00486 // origin parameter set but incorrect 00487 // Send a 403 response 00488 $message = HttpStatus::getMessage( 403 ); 00489 $response->header( "HTTP/1.1 403 $message", true, 403 ); 00490 $response->header( 'Cache-Control: no-cache' ); 00491 echo "'origin' parameter does not match Origin header\n"; 00492 00493 return false; 00494 } 00495 00496 $config = $this->getConfig(); 00497 $matchOrigin = self::matchOrigin( 00498 $originParam, 00499 $config->get( 'CrossSiteAJAXdomains' ), 00500 $config->get( 'CrossSiteAJAXdomainExceptions' ) 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 $response = $this->getRequest()->response(); 00558 $out = $this->getOutput(); 00559 00560 $config = $this->getConfig(); 00561 00562 if ( $config->get( 'VaryOnXFP' ) ) { 00563 $out->addVaryHeader( 'X-Forwarded-Proto' ); 00564 } 00565 00566 if ( $this->mCacheMode == 'private' ) { 00567 $response->header( 'Cache-Control: private' ); 00568 return; 00569 } 00570 00571 $useXVO = $config->get( 'UseXVO' ); 00572 if ( $this->mCacheMode == 'anon-public-user-private' ) { 00573 $out->addVaryHeader( 'Cookie' ); 00574 $response->header( $out->getVaryHeader() ); 00575 if ( $useXVO ) { 00576 $response->header( $out->getXVO() ); 00577 if ( $out->haveCacheVaryCookies() ) { 00578 // Logged in, mark this request private 00579 $response->header( 'Cache-Control: private' ); 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 ( $useXVO ) { 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 $result = $this->getResult(); 00648 00649 // Printer may not be initialized if the extractRequestParams() fails for the main module 00650 if ( !isset( $this->mPrinter ) ) { 00651 // The printer has not been created yet. Try to manually get formatter value. 00652 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT ); 00653 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) { 00654 $value = self::API_DEFAULT_FORMAT; 00655 } 00656 00657 $this->mPrinter = $this->createPrinterByName( $value ); 00658 } 00659 00660 // Printer may not be able to handle errors. This is particularly 00661 // likely if the module returns something for getCustomPrinter(). 00662 if ( !$this->mPrinter->canPrintErrors() ) { 00663 $this->mPrinter->safeProfileOut(); 00664 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT ); 00665 } 00666 00667 // Update raw mode flag for the selected printer. 00668 $result->setRawMode( $this->mPrinter->getNeedsRawData() ); 00669 00670 $config = $this->getConfig(); 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 // Something is seriously wrong 00682 if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) { 00683 $info = 'Database query error'; 00684 } else { 00685 $info = "Exception Caught: {$e->getMessage()}"; 00686 } 00687 00688 $errMessage = array( 00689 'code' => 'internal_api_error_' . get_class( $e ), 00690 'info' => $info, 00691 ); 00692 ApiResult::setContent( 00693 $errMessage, 00694 $config->get( 'ShowExceptionDetails' ) ? "\n\n{$e->getTraceAsString()}\n\n" : '' 00695 ); 00696 } 00697 00698 // Remember all the warnings to re-add them later 00699 $oldResult = $result->getData(); 00700 $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null; 00701 00702 $result->reset(); 00703 // Re-add the id 00704 $requestid = $this->getParameter( 'requestid' ); 00705 if ( !is_null( $requestid ) ) { 00706 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK ); 00707 } 00708 if ( $config->get( 'ShowHostnames' ) ) { 00709 // servedby is especially useful when debugging errors 00710 $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK ); 00711 } 00712 if ( $warnings !== null ) { 00713 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK ); 00714 } 00715 00716 $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK ); 00717 00718 return $errMessage['code']; 00719 } 00720 00725 protected function setupExecuteAction() { 00726 // First add the id to the top element 00727 $result = $this->getResult(); 00728 $requestid = $this->getParameter( 'requestid' ); 00729 if ( !is_null( $requestid ) ) { 00730 $result->addValue( null, 'requestid', $requestid ); 00731 } 00732 00733 if ( $this->getConfig()->get( 'ShowHostnames' ) ) { 00734 $servedby = $this->getParameter( 'servedby' ); 00735 if ( $servedby ) { 00736 $result->addValue( null, 'servedby', wfHostName() ); 00737 } 00738 } 00739 00740 if ( $this->getParameter( 'curtimestamp' ) ) { 00741 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ), 00742 ApiResult::NO_SIZE_CHECK ); 00743 } 00744 00745 $params = $this->extractRequestParams(); 00746 00747 $this->mAction = $params['action']; 00748 00749 if ( !is_string( $this->mAction ) ) { 00750 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); 00751 } 00752 00753 return $params; 00754 } 00755 00760 protected function setupModule() { 00761 // Instantiate the module requested by the user 00762 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' ); 00763 if ( $module === null ) { 00764 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); 00765 } 00766 $moduleParams = $module->extractRequestParams(); 00767 00768 // Check token, if necessary 00769 if ( $module->needsToken() === true ) { 00770 throw new MWException( 00771 "Module '{$module->getModuleName()}' must be updated for the new token handling. " . 00772 "See documentation for ApiBase::needsToken for details." 00773 ); 00774 } 00775 if ( $module->needsToken() ) { 00776 if ( !$module->mustBePosted() ) { 00777 throw new MWException( 00778 "Module '{$module->getModuleName()}' must require POST to use tokens." 00779 ); 00780 } 00781 00782 if ( !isset( $moduleParams['token'] ) ) { 00783 $this->dieUsageMsg( array( 'missingparam', 'token' ) ); 00784 } 00785 00786 if ( !$this->getConfig()->get( 'DebugAPI' ) && 00787 array_key_exists( 00788 $module->encodeParamName( 'token' ), 00789 $this->getRequest()->getQueryValues() 00790 ) 00791 ) { 00792 $this->dieUsage( 00793 "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body", 00794 'mustposttoken' 00795 ); 00796 } 00797 00798 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) { 00799 $this->dieUsageMsg( 'sessionfailure' ); 00800 } 00801 } 00802 00803 return $module; 00804 } 00805 00812 protected function checkMaxLag( $module, $params ) { 00813 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) { 00814 // Check for maxlag 00815 $maxLag = $params['maxlag']; 00816 list( $host, $lag ) = wfGetLB()->getMaxLag(); 00817 if ( $lag > $maxLag ) { 00818 $response = $this->getRequest()->response(); 00819 00820 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); 00821 $response->header( 'X-Database-Lag: ' . intval( $lag ) ); 00822 00823 if ( $this->getConfig()->get( 'ShowHostnames' ) ) { 00824 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); 00825 } 00826 00827 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); 00828 } 00829 } 00830 00831 return true; 00832 } 00833 00838 protected function checkExecutePermissions( $module ) { 00839 $user = $this->getUser(); 00840 if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) && 00841 !$user->isAllowed( 'read' ) 00842 ) { 00843 $this->dieUsageMsg( 'readrequired' ); 00844 } 00845 if ( $module->isWriteMode() ) { 00846 if ( !$this->mEnableWrite ) { 00847 $this->dieUsageMsg( 'writedisabled' ); 00848 } 00849 if ( !$user->isAllowed( 'writeapi' ) ) { 00850 $this->dieUsageMsg( 'writerequired' ); 00851 } 00852 if ( wfReadOnly() ) { 00853 $this->dieReadOnly(); 00854 } 00855 } 00856 00857 // Allow extensions to stop execution for arbitrary reasons. 00858 $message = false; 00859 if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) { 00860 $this->dieUsageMsg( $message ); 00861 } 00862 } 00863 00868 protected function checkAsserts( $params ) { 00869 if ( isset( $params['assert'] ) ) { 00870 $user = $this->getUser(); 00871 switch ( $params['assert'] ) { 00872 case 'user': 00873 if ( $user->isAnon() ) { 00874 $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' ); 00875 } 00876 break; 00877 case 'bot': 00878 if ( !$user->isAllowed( 'bot' ) ) { 00879 $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' ); 00880 } 00881 break; 00882 } 00883 } 00884 } 00885 00891 protected function setupExternalResponse( $module, $params ) { 00892 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) { 00893 // Module requires POST. GET request might still be allowed 00894 // if $wgDebugApi is true, otherwise fail. 00895 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) ); 00896 } 00897 00898 // See if custom printer is used 00899 $this->mPrinter = $module->getCustomPrinter(); 00900 if ( is_null( $this->mPrinter ) ) { 00901 // Create an appropriate printer 00902 $this->mPrinter = $this->createPrinterByName( $params['format'] ); 00903 } 00904 00905 if ( $this->mPrinter->getNeedsRawData() ) { 00906 $this->getResult()->setRawMode(); 00907 } 00908 } 00909 00913 protected function executeAction() { 00914 $params = $this->setupExecuteAction(); 00915 $module = $this->setupModule(); 00916 $this->mModule = $module; 00917 00918 $this->checkExecutePermissions( $module ); 00919 00920 if ( !$this->checkMaxLag( $module, $params ) ) { 00921 return; 00922 } 00923 00924 if ( !$this->mInternalMode ) { 00925 $this->setupExternalResponse( $module, $params ); 00926 } 00927 00928 $this->checkAsserts( $params ); 00929 00930 // Execute 00931 $module->profileIn(); 00932 $module->execute(); 00933 wfRunHooks( 'APIAfterExecute', array( &$module ) ); 00934 $module->profileOut(); 00935 00936 $this->reportUnusedParams(); 00937 00938 if ( !$this->mInternalMode ) { 00939 //append Debug information 00940 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() ); 00941 00942 // Print result data 00943 $this->printResult( false ); 00944 } 00945 } 00946 00951 protected function logRequest( $time ) { 00952 $request = $this->getRequest(); 00953 $milliseconds = $time === null ? '?' : round( $time * 1000 ); 00954 $s = 'API' . 00955 ' ' . $request->getMethod() . 00956 ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . 00957 ' ' . $request->getIP() . 00958 ' T=' . $milliseconds . 'ms'; 00959 foreach ( $this->getParamsUsed() as $name ) { 00960 $value = $request->getVal( $name ); 00961 if ( $value === null ) { 00962 continue; 00963 } 00964 $s .= ' ' . $name . '='; 00965 if ( strlen( $value ) > 256 ) { 00966 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) ); 00967 $s .= $encValue . '[...]'; 00968 } else { 00969 $s .= $this->encodeRequestLogValue( $value ); 00970 } 00971 } 00972 $s .= "\n"; 00973 wfDebugLog( 'api', $s, 'private' ); 00974 } 00975 00981 protected function encodeRequestLogValue( $s ) { 00982 static $table; 00983 if ( !$table ) { 00984 $chars = ';@$!*(),/:'; 00985 $numChars = strlen( $chars ); 00986 for ( $i = 0; $i < $numChars; $i++ ) { 00987 $table[rawurlencode( $chars[$i] )] = $chars[$i]; 00988 } 00989 } 00990 00991 return strtr( rawurlencode( $s ), $table ); 00992 } 00993 00998 protected function getParamsUsed() { 00999 return array_keys( $this->mParamsUsed ); 01000 } 01001 01008 public function getVal( $name, $default = null ) { 01009 $this->mParamsUsed[$name] = true; 01010 01011 $ret = $this->getRequest()->getVal( $name ); 01012 if ( $ret === null ) { 01013 if ( $this->getRequest()->getArray( $name ) !== null ) { 01014 // See bug 10262 for why we don't just join( '|', ... ) the 01015 // array. 01016 $this->setWarning( 01017 "Parameter '$name' uses unsupported PHP array syntax" 01018 ); 01019 } 01020 $ret = $default; 01021 } 01022 return $ret; 01023 } 01024 01031 public function getCheck( $name ) { 01032 return $this->getVal( $name, null ) !== null; 01033 } 01034 01042 public function getUpload( $name ) { 01043 $this->mParamsUsed[$name] = true; 01044 01045 return $this->getRequest()->getUpload( $name ); 01046 } 01047 01052 protected function reportUnusedParams() { 01053 $paramsUsed = $this->getParamsUsed(); 01054 $allParams = $this->getRequest()->getValueNames(); 01055 01056 if ( !$this->mInternalMode ) { 01057 // Printer has not yet executed; don't warn that its parameters are unused 01058 $printerParams = array_map( 01059 array( $this->mPrinter, 'encodeParamName' ), 01060 array_keys( $this->mPrinter->getFinalParams() ?: array() ) 01061 ); 01062 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams ); 01063 } else { 01064 $unusedParams = array_diff( $allParams, $paramsUsed ); 01065 } 01066 01067 if ( count( $unusedParams ) ) { 01068 $s = count( $unusedParams ) > 1 ? 's' : ''; 01069 $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" ); 01070 } 01071 } 01072 01078 protected function printResult( $isError ) { 01079 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) { 01080 $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' ); 01081 } 01082 01083 $this->getResult()->cleanUpUTF8(); 01084 $printer = $this->mPrinter; 01085 $printer->profileIn(); 01086 01092 $isHelp = $isError || $this->mAction == 'help'; 01093 $printer->setUnescapeAmps( $isHelp && $printer->getFormat() == 'XML' && $printer->getIsHtml() ); 01094 01095 $printer->initPrinter( $isHelp ); 01096 01097 $printer->execute(); 01098 $printer->closePrinter(); 01099 $printer->profileOut(); 01100 } 01101 01105 public function isReadMode() { 01106 return false; 01107 } 01108 01114 public function getAllowedParams() { 01115 return array( 01116 'format' => array( 01117 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT, 01118 ApiBase::PARAM_TYPE => 'submodule', 01119 ), 01120 'action' => array( 01121 ApiBase::PARAM_DFLT => 'help', 01122 ApiBase::PARAM_TYPE => 'submodule', 01123 ), 01124 'maxlag' => array( 01125 ApiBase::PARAM_TYPE => 'integer' 01126 ), 01127 'smaxage' => array( 01128 ApiBase::PARAM_TYPE => 'integer', 01129 ApiBase::PARAM_DFLT => 0 01130 ), 01131 'maxage' => array( 01132 ApiBase::PARAM_TYPE => 'integer', 01133 ApiBase::PARAM_DFLT => 0 01134 ), 01135 'assert' => array( 01136 ApiBase::PARAM_TYPE => array( 'user', 'bot' ) 01137 ), 01138 'requestid' => null, 01139 'servedby' => false, 01140 'curtimestamp' => false, 01141 'origin' => null, 01142 ); 01143 } 01144 01150 public function getParamDescription() { 01151 return array( 01152 'format' => 'The format of the output', 01153 'action' => 'What action you would like to perform. See below for module help', 01154 'maxlag' => array( 01155 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.', 01156 'To save actions causing any more site replication lag, this parameter can make the client', 01157 'wait until the replication lag is less than the specified value.', 01158 'In case of a replag error, error code "maxlag" is returned, with the message like', 01159 '"Waiting for $host: $lag seconds lagged\n".', 01160 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information', 01161 ), 01162 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached', 01163 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', 01164 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"', 01165 'requestid' => 'Request ID to distinguish requests. This will just be output back to you', 01166 'servedby' => 'Include the hostname that served the request in the ' . 01167 'results. Unconditionally shown on error', 01168 'curtimestamp' => 'Include the current timestamp in the result.', 01169 'origin' => array( 01170 'When accessing the API using a cross-domain AJAX request (CORS), set this to the', 01171 'originating domain. This must be included in any pre-flight request, and', 01172 'therefore must be part of the request URI (not the POST body). This must match', 01173 'one of the origins in the Origin: header exactly, so it has to be set to ', 01174 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this', 01175 'parameter does not match the Origin: header, a 403 response will be returned. If', 01176 'this parameter matches the Origin: header and the origin is whitelisted, an', 01177 'Access-Control-Allow-Origin header will be set.', 01178 ), 01179 ); 01180 } 01181 01187 public function getDescription() { 01188 return array( 01189 '', 01190 '', 01191 '**********************************************************************************************', 01192 '** **', 01193 '** This is an auto-generated MediaWiki API documentation page **', 01194 '** **', 01195 '** Documentation and Examples: **', 01196 '** https://www.mediawiki.org/wiki/API **', 01197 '** **', 01198 '**********************************************************************************************', 01199 '', 01200 'Status: All features shown on this page should be working, but the API', 01201 ' is still in active development, and may change at any time.', 01202 ' Make sure to monitor our mailing list for any updates.', 01203 '', 01204 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent', 01205 ' with the key "MediaWiki-API-Error" and then both the value of the', 01206 ' header and the error code sent back will be set to the same value.', 01207 '', 01208 ' In the case of an invalid action being passed, these will have a value', 01209 ' of "unknown_action".', 01210 '', 01211 ' For more information see https://www.mediawiki.org' . 01212 '/wiki/API:Errors_and_warnings', 01213 '', 01214 'Documentation: https://www.mediawiki.org/wiki/API:Main_page', 01215 'FAQ https://www.mediawiki.org/wiki/API:FAQ', 01216 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api', 01217 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce', 01218 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' . 01219 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts', 01220 '', 01221 '', 01222 '', 01223 '', 01224 '', 01225 ); 01226 } 01227 01232 protected function getCredits() { 01233 return array( 01234 'API developers:', 01235 ' Roan Kattouw (lead developer Sep 2007-2009)', 01236 ' Victor Vasiliev', 01237 ' Bryan Tong Minh', 01238 ' Sam Reed', 01239 ' Yuri Astrakhan (creator, lead developer Sep 2006-Sep 2007, 2012-2013)', 01240 ' Brad Jorsch (lead developer 2013-now)', 01241 '', 01242 'Please send your comments, suggestions and questions to [email protected]', 01243 'or file a bug report at https://bugzilla.wikimedia.org/' 01244 ); 01245 } 01246 01252 public function setHelp( $help = true ) { 01253 $this->mPrinter->setHelp( $help ); 01254 } 01255 01261 public function makeHelpMsg() { 01262 global $wgMemc; 01263 $this->setHelp(); 01264 // Get help text from cache if present 01265 $key = wfMemcKey( 'apihelp', $this->getModuleName(), 01266 str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) ); 01267 01268 $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' ); 01269 if ( $cacheHelpTimeout > 0 ) { 01270 $cached = $wgMemc->get( $key ); 01271 if ( $cached ) { 01272 return $cached; 01273 } 01274 } 01275 $retval = $this->reallyMakeHelpMsg(); 01276 if ( $cacheHelpTimeout > 0 ) { 01277 $wgMemc->set( $key, $retval, $cacheHelpTimeout ); 01278 } 01279 01280 return $retval; 01281 } 01282 01286 public function reallyMakeHelpMsg() { 01287 $this->setHelp(); 01288 01289 // Use parent to make default message for the main module 01290 $msg = parent::makeHelpMsg(); 01291 01292 $astriks = str_repeat( '*** ', 14 ); 01293 $msg .= "\n\n$astriks Modules $astriks\n\n"; 01294 01295 foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) { 01296 $module = $this->mModuleMgr->getModule( $name ); 01297 $msg .= self::makeHelpMsgHeader( $module, 'action' ); 01298 01299 $msg2 = $module->makeHelpMsg(); 01300 if ( $msg2 !== false ) { 01301 $msg .= $msg2; 01302 } 01303 $msg .= "\n"; 01304 } 01305 01306 $msg .= "\n$astriks Permissions $astriks\n\n"; 01307 foreach ( self::$mRights as $right => $rightMsg ) { 01308 $groups = User::getGroupsWithPermission( $right ); 01309 $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg['msg'], $rightMsg['params'] ) . 01310 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n"; 01311 } 01312 01313 $msg .= "\n$astriks Formats $astriks\n\n"; 01314 foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) { 01315 $module = $this->mModuleMgr->getModule( $name ); 01316 $msg .= self::makeHelpMsgHeader( $module, 'format' ); 01317 $msg2 = $module->makeHelpMsg(); 01318 if ( $msg2 !== false ) { 01319 $msg .= $msg2; 01320 } 01321 $msg .= "\n"; 01322 } 01323 01324 $msg .= "\n*** Credits: ***\n " . implode( "\n ", $this->getCredits() ) . "\n"; 01325 01326 return $msg; 01327 } 01328 01335 public static function makeHelpMsgHeader( $module, $paramName ) { 01336 $modulePrefix = $module->getModulePrefix(); 01337 if ( strval( $modulePrefix ) !== '' ) { 01338 $modulePrefix = "($modulePrefix) "; 01339 } 01340 01341 return "* $paramName={$module->getModuleName()} $modulePrefix*"; 01342 } 01343 01344 private $mCanApiHighLimits = null; 01345 01350 public function canApiHighLimits() { 01351 if ( !isset( $this->mCanApiHighLimits ) ) { 01352 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' ); 01353 } 01354 01355 return $this->mCanApiHighLimits; 01356 } 01357 01363 public function getShowVersions() { 01364 wfDeprecated( __METHOD__, '1.21' ); 01365 01366 return false; 01367 } 01368 01373 public function getModuleManager() { 01374 return $this->mModuleMgr; 01375 } 01376 01386 protected function addModule( $name, $class ) { 01387 $this->getModuleManager()->addModule( $name, 'action', $class ); 01388 } 01389 01398 protected function addFormat( $name, $class ) { 01399 $this->getModuleManager()->addModule( $name, 'format', $class ); 01400 } 01401 01407 function getModules() { 01408 return $this->getModuleManager()->getNamesWithClasses( 'action' ); 01409 } 01410 01418 public function getFormats() { 01419 return $this->getModuleManager()->getNamesWithClasses( 'format' ); 01420 } 01421 } 01422 01429 class UsageException extends MWException { 01430 01431 private $mCodestr; 01432 01436 private $mExtraData; 01437 01444 public function __construct( $message, $codestr, $code = 0, $extradata = null ) { 01445 parent::__construct( $message, $code ); 01446 $this->mCodestr = $codestr; 01447 $this->mExtraData = $extradata; 01448 } 01449 01453 public function getCodeString() { 01454 return $this->mCodestr; 01455 } 01456 01460 public function getMessageArray() { 01461 $result = array( 01462 'code' => $this->mCodestr, 01463 'info' => $this->getMessage() 01464 ); 01465 if ( is_array( $this->mExtraData ) ) { 01466 $result = array_merge( $result, $this->mExtraData ); 01467 } 01468 01469 return $result; 01470 } 01471 01475 public function __toString() { 01476 return "{$this->getCodeString()}: {$this->getMessage()}"; 01477 } 01478 }