MediaWiki  REL1_22
ApiMain.php
Go to the documentation of this file.
00001 <?php
00041 class ApiMain extends ApiBase {
00042 
00046     const API_DEFAULT_FORMAT = 'xmlfm';
00047 
00051     private static $Modules = array(
00052         'login' => 'ApiLogin',
00053         'logout' => 'ApiLogout',
00054         'createaccount' => 'ApiCreateAccount',
00055         'query' => 'ApiQuery',
00056         'expandtemplates' => 'ApiExpandTemplates',
00057         'parse' => 'ApiParse',
00058         'opensearch' => 'ApiOpenSearch',
00059         'feedcontributions' => 'ApiFeedContributions',
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     );
00088 
00092     private static $Formats = array(
00093         'json' => 'ApiFormatJson',
00094         'jsonfm' => 'ApiFormatJson',
00095         'php' => 'ApiFormatPhp',
00096         'phpfm' => 'ApiFormatPhp',
00097         'wddx' => 'ApiFormatWddx',
00098         'wddxfm' => 'ApiFormatWddx',
00099         'xml' => 'ApiFormatXml',
00100         'xmlfm' => 'ApiFormatXml',
00101         'yaml' => 'ApiFormatYaml',
00102         'yamlfm' => 'ApiFormatYaml',
00103         'rawfm' => 'ApiFormatJson',
00104         'txt' => 'ApiFormatTxt',
00105         'txtfm' => 'ApiFormatTxt',
00106         'dbg' => 'ApiFormatDbg',
00107         'dbgfm' => 'ApiFormatDbg',
00108         'dump' => 'ApiFormatDump',
00109         'dumpfm' => 'ApiFormatDump',
00110         'none' => 'ApiFormatNone',
00111     );
00112 
00119     private static $mRights = array(
00120         'writeapi' => array(
00121             'msg' => 'Use of the write API',
00122             'params' => array()
00123         ),
00124         'apihighlimits' => array(
00125             '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.',
00126             'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 )
00127         )
00128     );
00129 
00133     private $mPrinter;
00134 
00135     private $mModuleMgr, $mResult;
00136     private $mAction;
00137     private $mEnableWrite;
00138     private $mInternalMode, $mSquidMaxage, $mModule;
00139 
00140     private $mCacheMode = 'private';
00141     private $mCacheControl = array();
00142     private $mParamsUsed = array();
00143 
00150     public function __construct( $context = null, $enableWrite = false ) {
00151         if ( $context === null ) {
00152             $context = RequestContext::getMain();
00153         } elseif ( $context instanceof WebRequest ) {
00154             // BC for pre-1.19
00155             $request = $context;
00156             $context = RequestContext::getMain();
00157         }
00158         // We set a derivative context so we can change stuff later
00159         $this->setContext( new DerivativeContext( $context ) );
00160 
00161         if ( isset( $request ) ) {
00162             $this->getContext()->setRequest( $request );
00163         }
00164 
00165         $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
00166 
00167         // Special handling for the main module: $parent === $this
00168         parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
00169 
00170         if ( !$this->mInternalMode ) {
00171             // Impose module restrictions.
00172             // If the current user cannot read,
00173             // Remove all modules other than login
00174             global $wgUser;
00175 
00176             if ( $this->getVal( 'callback' ) !== null ) {
00177                 // JSON callback allows cross-site reads.
00178                 // For safety, strip user credentials.
00179                 wfDebug( "API: stripping user credentials for JSON callback\n" );
00180                 $wgUser = new User();
00181                 $this->getContext()->setUser( $wgUser );
00182             }
00183         }
00184 
00185         global $wgAPIModules;
00186         $this->mModuleMgr = new ApiModuleManager( $this );
00187         $this->mModuleMgr->addModules( self::$Modules, 'action' );
00188         $this->mModuleMgr->addModules( $wgAPIModules, 'action' );
00189         $this->mModuleMgr->addModules( self::$Formats, 'format' );
00190 
00191         $this->mResult = new ApiResult( $this );
00192         $this->mEnableWrite = $enableWrite;
00193 
00194         $this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling()
00195         $this->mCommit = false;
00196     }
00197 
00202     public function isInternalMode() {
00203         return $this->mInternalMode;
00204     }
00205 
00211     public function getResult() {
00212         return $this->mResult;
00213     }
00214 
00220     public function getModule() {
00221         return $this->mModule;
00222     }
00223 
00229     public function getPrinter() {
00230         return $this->mPrinter;
00231     }
00232 
00238     public function setCacheMaxAge( $maxage ) {
00239         $this->setCacheControl( array(
00240             'max-age' => $maxage,
00241             's-maxage' => $maxage
00242         ) );
00243     }
00244 
00270     public function setCacheMode( $mode ) {
00271         if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) {
00272             wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
00273             // Ignore for forwards-compatibility
00274             return;
00275         }
00276 
00277         if ( !User::isEveryoneAllowed( 'read' ) ) {
00278             // Private wiki, only private headers
00279             if ( $mode !== 'private' ) {
00280                 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
00281                 return;
00282             }
00283         }
00284 
00285         wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
00286         $this->mCacheMode = $mode;
00287     }
00288 
00294     public function setCachePrivate() {
00295         wfDeprecated( __METHOD__, '1.17' );
00296         $this->setCacheMode( 'private' );
00297     }
00298 
00309     public function setCacheControl( $directives ) {
00310         $this->mCacheControl = $directives + $this->mCacheControl;
00311     }
00312 
00323     public function setVaryCookie() {
00324         wfDeprecated( __METHOD__, '1.17' );
00325         $this->setCacheMode( 'anon-public-user-private' );
00326     }
00327 
00335     public function createPrinterByName( $format ) {
00336         $printer = $this->mModuleMgr->getModule( $format, 'format' );
00337         if ( $printer === null ) {
00338             $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
00339         }
00340         return $printer;
00341     }
00342 
00346     public function execute() {
00347         $this->profileIn();
00348         if ( $this->mInternalMode ) {
00349             $this->executeAction();
00350         } else {
00351             $this->executeActionWithErrorHandling();
00352         }
00353 
00354         $this->profileOut();
00355     }
00356 
00361     protected function executeActionWithErrorHandling() {
00362         // Verify the CORS header before executing the action
00363         if ( !$this->handleCORS() ) {
00364             // handleCORS() has sent a 403, abort
00365             return;
00366         }
00367 
00368         // Exit here if the request method was OPTIONS
00369         // (assume there will be a followup GET or POST)
00370         if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
00371             return;
00372         }
00373 
00374         // In case an error occurs during data output,
00375         // clear the output buffer and print just the error information
00376         ob_start();
00377 
00378         $t = microtime( true );
00379         try {
00380             $this->executeAction();
00381         } catch ( Exception $e ) {
00382             // Allow extra cleanup and logging
00383             wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
00384 
00385             // Log it
00386             if ( !( $e instanceof UsageException ) ) {
00387                 MWExceptionHandler::logException( $e );
00388             }
00389 
00390             // Handle any kind of exception by outputting properly formatted error message.
00391             // If this fails, an unhandled exception should be thrown so that global error
00392             // handler will process and log it.
00393 
00394             $errCode = $this->substituteResultWithError( $e );
00395 
00396             // Error results should not be cached
00397             $this->setCacheMode( 'private' );
00398 
00399             $response = $this->getRequest()->response();
00400             $headerStr = 'MediaWiki-API-Error: ' . $errCode;
00401             if ( $e->getCode() === 0 ) {
00402                 $response->header( $headerStr );
00403             } else {
00404                 $response->header( $headerStr, true, $e->getCode() );
00405             }
00406 
00407             // Reset and print just the error message
00408             ob_clean();
00409 
00410             // If the error occurred during printing, do a printer->profileOut()
00411             $this->mPrinter->safeProfileOut();
00412             $this->printResult( true );
00413         }
00414 
00415         // Log the request whether or not there was an error
00416         $this->logRequest( microtime( true ) - $t );
00417 
00418         // Send cache headers after any code which might generate an error, to
00419         // avoid sending public cache headers for errors.
00420         $this->sendCacheHeaders();
00421 
00422         if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) {
00423             echo wfReportTime();
00424         }
00425 
00426         ob_end_flush();
00427     }
00428 
00441     protected function handleCORS() {
00442         global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions;
00443 
00444         $originParam = $this->getParameter( 'origin' ); // defaults to null
00445         if ( $originParam === null ) {
00446             // No origin parameter, nothing to do
00447             return true;
00448         }
00449 
00450         $request = $this->getRequest();
00451         $response = $request->response();
00452         // Origin: header is a space-separated list of origins, check all of them
00453         $originHeader = $request->getHeader( 'Origin' );
00454         if ( $originHeader === false ) {
00455             $origins = array();
00456         } else {
00457             $origins = explode( ' ', $originHeader );
00458         }
00459         if ( !in_array( $originParam, $origins ) ) {
00460             // origin parameter set but incorrect
00461             // Send a 403 response
00462             $message = HttpStatus::getMessage( 403 );
00463             $response->header( "HTTP/1.1 403 $message", true, 403 );
00464             $response->header( 'Cache-Control: no-cache' );
00465             echo "'origin' parameter does not match Origin header\n";
00466             return false;
00467         }
00468         if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) {
00469             $response->header( "Access-Control-Allow-Origin: $originParam" );
00470             $response->header( 'Access-Control-Allow-Credentials: true' );
00471             $this->getOutput()->addVaryHeader( 'Origin' );
00472         }
00473         return true;
00474     }
00475 
00483     protected static function matchOrigin( $value, $rules, $exceptions ) {
00484         foreach ( $rules as $rule ) {
00485             if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
00486                 // Rule matches, check exceptions
00487                 foreach ( $exceptions as $exc ) {
00488                     if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
00489                         return false;
00490                     }
00491                 }
00492                 return true;
00493             }
00494         }
00495         return false;
00496     }
00497 
00506     protected static function wildcardToRegex( $wildcard ) {
00507         $wildcard = preg_quote( $wildcard, '/' );
00508         $wildcard = str_replace(
00509             array( '\*', '\?' ),
00510             array( '.*?', '.' ),
00511             $wildcard
00512         );
00513         return "/https?:\/\/$wildcard/";
00514     }
00515 
00516     protected function sendCacheHeaders() {
00517         global $wgUseXVO, $wgVaryOnXFP;
00518         $response = $this->getRequest()->response();
00519         $out = $this->getOutput();
00520 
00521         if ( $wgVaryOnXFP ) {
00522             $out->addVaryHeader( 'X-Forwarded-Proto' );
00523         }
00524 
00525         if ( $this->mCacheMode == 'private' ) {
00526             $response->header( 'Cache-Control: private' );
00527             return;
00528         }
00529 
00530         if ( $this->mCacheMode == 'anon-public-user-private' ) {
00531             $out->addVaryHeader( 'Cookie' );
00532             $response->header( $out->getVaryHeader() );
00533             if ( $wgUseXVO ) {
00534                 $response->header( $out->getXVO() );
00535                 if ( $out->haveCacheVaryCookies() ) {
00536                     // Logged in, mark this request private
00537                     $response->header( 'Cache-Control: private' );
00538                     return;
00539                 }
00540                 // Logged out, send normal public headers below
00541             } elseif ( session_id() != '' ) {
00542                 // Logged in or otherwise has session (e.g. anonymous users who have edited)
00543                 // Mark request private
00544                 $response->header( 'Cache-Control: private' );
00545                 return;
00546             } // else no XVO and anonymous, send public headers below
00547         }
00548 
00549         // Send public headers
00550         $response->header( $out->getVaryHeader() );
00551         if ( $wgUseXVO ) {
00552             $response->header( $out->getXVO() );
00553         }
00554 
00555         // If nobody called setCacheMaxAge(), use the (s)maxage parameters
00556         if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
00557             $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
00558         }
00559         if ( !isset( $this->mCacheControl['max-age'] ) ) {
00560             $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
00561         }
00562 
00563         if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
00564             // Public cache not requested
00565             // Sending a Vary header in this case is harmless, and protects us
00566             // against conditional calls of setCacheMaxAge().
00567             $response->header( 'Cache-Control: private' );
00568             return;
00569         }
00570 
00571         $this->mCacheControl['public'] = true;
00572 
00573         // Send an Expires header
00574         $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
00575         $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
00576         $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
00577 
00578         // Construct the Cache-Control header
00579         $ccHeader = '';
00580         $separator = '';
00581         foreach ( $this->mCacheControl as $name => $value ) {
00582             if ( is_bool( $value ) ) {
00583                 if ( $value ) {
00584                     $ccHeader .= $separator . $name;
00585                     $separator = ', ';
00586                 }
00587             } else {
00588                 $ccHeader .= $separator . "$name=$value";
00589                 $separator = ', ';
00590             }
00591         }
00592 
00593         $response->header( "Cache-Control: $ccHeader" );
00594     }
00595 
00602     protected function substituteResultWithError( $e ) {
00603         global $wgShowHostnames;
00604 
00605         $result = $this->getResult();
00606         // Printer may not be initialized if the extractRequestParams() fails for the main module
00607         if ( !isset( $this->mPrinter ) ) {
00608             // The printer has not been created yet. Try to manually get formatter value.
00609             $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
00610             if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
00611                 $value = self::API_DEFAULT_FORMAT;
00612             }
00613 
00614             $this->mPrinter = $this->createPrinterByName( $value );
00615             if ( $this->mPrinter->getNeedsRawData() ) {
00616                 $result->setRawMode();
00617             }
00618         }
00619 
00620         if ( $e instanceof UsageException ) {
00621             // User entered incorrect parameters - print usage screen
00622             $errMessage = $e->getMessageArray();
00623 
00624             // Only print the help message when this is for the developer, not runtime
00625             if ( $this->mPrinter->getWantsHelp() || $this->mAction == 'help' ) {
00626                 ApiResult::setContent( $errMessage, $this->makeHelpMsg() );
00627             }
00628         } else {
00629             global $wgShowSQLErrors, $wgShowExceptionDetails;
00630             // Something is seriously wrong
00631             if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) {
00632                 $info = 'Database query error';
00633             } else {
00634                 $info = "Exception Caught: {$e->getMessage()}";
00635             }
00636 
00637             $errMessage = array(
00638                 'code' => 'internal_api_error_' . get_class( $e ),
00639                 'info' => $info,
00640             );
00641             ApiResult::setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' );
00642         }
00643 
00644         // Remember all the warnings to re-add them later
00645         $oldResult = $result->getData();
00646         $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
00647 
00648         $result->reset();
00649         $result->disableSizeCheck();
00650         // Re-add the id
00651         $requestid = $this->getParameter( 'requestid' );
00652         if ( !is_null( $requestid ) ) {
00653             $result->addValue( null, 'requestid', $requestid );
00654         }
00655         if ( $wgShowHostnames ) {
00656             // servedby is especially useful when debugging errors
00657             $result->addValue( null, 'servedby', wfHostName() );
00658         }
00659         if ( $warnings !== null ) {
00660             $result->addValue( null, 'warnings', $warnings );
00661         }
00662 
00663         $result->addValue( null, 'error', $errMessage );
00664 
00665         return $errMessage['code'];
00666     }
00667 
00672     protected function setupExecuteAction() {
00673         global $wgShowHostnames;
00674 
00675         // First add the id to the top element
00676         $result = $this->getResult();
00677         $requestid = $this->getParameter( 'requestid' );
00678         if ( !is_null( $requestid ) ) {
00679             $result->addValue( null, 'requestid', $requestid );
00680         }
00681 
00682         if ( $wgShowHostnames ) {
00683             $servedby = $this->getParameter( 'servedby' );
00684             if ( $servedby ) {
00685                 $result->addValue( null, 'servedby', wfHostName() );
00686             }
00687         }
00688 
00689         $params = $this->extractRequestParams();
00690 
00691         $this->mAction = $params['action'];
00692 
00693         if ( !is_string( $this->mAction ) ) {
00694             $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
00695         }
00696 
00697         return $params;
00698     }
00699 
00704     protected function setupModule() {
00705         // Instantiate the module requested by the user
00706         $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
00707         if ( $module === null ) {
00708             $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
00709         }
00710         $moduleParams = $module->extractRequestParams();
00711 
00712         // Die if token required, but not provided
00713         $salt = $module->getTokenSalt();
00714         if ( $salt !== false ) {
00715             if ( !isset( $moduleParams['token'] ) ) {
00716                 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
00717             } else {
00718                 if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getContext()->getRequest() ) ) {
00719                     $this->dieUsageMsg( 'sessionfailure' );
00720                 }
00721             }
00722         }
00723         return $module;
00724     }
00725 
00732     protected function checkMaxLag( $module, $params ) {
00733         if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
00734             // Check for maxlag
00735             global $wgShowHostnames;
00736             $maxLag = $params['maxlag'];
00737             list( $host, $lag ) = wfGetLB()->getMaxLag();
00738             if ( $lag > $maxLag ) {
00739                 $response = $this->getRequest()->response();
00740 
00741                 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
00742                 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
00743 
00744                 if ( $wgShowHostnames ) {
00745                     $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
00746                 } else {
00747                     $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
00748                 }
00749                 return false;
00750             }
00751         }
00752         return true;
00753     }
00754 
00759     protected function checkExecutePermissions( $module ) {
00760         $user = $this->getUser();
00761         if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
00762             !$user->isAllowed( 'read' ) )
00763         {
00764             $this->dieUsageMsg( 'readrequired' );
00765         }
00766         if ( $module->isWriteMode() ) {
00767             if ( !$this->mEnableWrite ) {
00768                 $this->dieUsageMsg( 'writedisabled' );
00769             }
00770             if ( !$user->isAllowed( 'writeapi' ) ) {
00771                 $this->dieUsageMsg( 'writerequired' );
00772             }
00773             if ( wfReadOnly() ) {
00774                 $this->dieReadOnly();
00775             }
00776         }
00777 
00778         // Allow extensions to stop execution for arbitrary reasons.
00779         $message = false;
00780         if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
00781             $this->dieUsageMsg( $message );
00782         }
00783     }
00784 
00790     protected function setupExternalResponse( $module, $params ) {
00791         if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
00792             // Module requires POST. GET request might still be allowed
00793             // if $wgDebugApi is true, otherwise fail.
00794             $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) );
00795         }
00796 
00797         // See if custom printer is used
00798         $this->mPrinter = $module->getCustomPrinter();
00799         if ( is_null( $this->mPrinter ) ) {
00800             // Create an appropriate printer
00801             $this->mPrinter = $this->createPrinterByName( $params['format'] );
00802         }
00803 
00804         if ( $this->mPrinter->getNeedsRawData() ) {
00805             $this->getResult()->setRawMode();
00806         }
00807     }
00808 
00812     protected function executeAction() {
00813         $params = $this->setupExecuteAction();
00814         $module = $this->setupModule();
00815         $this->mModule = $module;
00816 
00817         $this->checkExecutePermissions( $module );
00818 
00819         if ( !$this->checkMaxLag( $module, $params ) ) {
00820             return;
00821         }
00822 
00823         if ( !$this->mInternalMode ) {
00824             $this->setupExternalResponse( $module, $params );
00825         }
00826 
00827         // Execute
00828         $module->profileIn();
00829         $module->execute();
00830         wfRunHooks( 'APIAfterExecute', array( &$module ) );
00831         $module->profileOut();
00832 
00833         $this->reportUnusedParams();
00834 
00835         if ( !$this->mInternalMode ) {
00836             //append Debug information
00837             MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
00838 
00839             // Print result data
00840             $this->printResult( false );
00841         }
00842     }
00843 
00848     protected function logRequest( $time ) {
00849         $request = $this->getRequest();
00850         $milliseconds = $time === null ? '?' : round( $time * 1000 );
00851         $s = 'API' .
00852             ' ' . $request->getMethod() .
00853             ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
00854             ' ' . $request->getIP() .
00855             ' T=' . $milliseconds . 'ms';
00856         foreach ( $this->getParamsUsed() as $name ) {
00857             $value = $request->getVal( $name );
00858             if ( $value === null ) {
00859                 continue;
00860             }
00861             $s .= ' ' . $name . '=';
00862             if ( strlen( $value ) > 256 ) {
00863                 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
00864                 $s .= $encValue . '[...]';
00865             } else {
00866                 $s .= $this->encodeRequestLogValue( $value );
00867             }
00868         }
00869         $s .= "\n";
00870         wfDebugLog( 'api', $s, false );
00871     }
00872 
00876     protected function encodeRequestLogValue( $s ) {
00877         static $table;
00878         if ( !$table ) {
00879             $chars = ';@$!*(),/:';
00880             for ( $i = 0; $i < strlen( $chars ); $i++ ) {
00881                 $table[rawurlencode( $chars[$i] )] = $chars[$i];
00882             }
00883         }
00884         return strtr( rawurlencode( $s ), $table );
00885     }
00886 
00890     protected function getParamsUsed() {
00891         return array_keys( $this->mParamsUsed );
00892     }
00893 
00897     public function getVal( $name, $default = null ) {
00898         $this->mParamsUsed[$name] = true;
00899         return $this->getRequest()->getVal( $name, $default );
00900     }
00901 
00906     public function getCheck( $name ) {
00907         $this->mParamsUsed[$name] = true;
00908         return $this->getRequest()->getCheck( $name );
00909     }
00910 
00918     public function getUpload( $name ) {
00919         $this->mParamsUsed[$name] = true;
00920         return $this->getRequest()->getUpload( $name );
00921     }
00922 
00927     protected function reportUnusedParams() {
00928         $paramsUsed = $this->getParamsUsed();
00929         $allParams = $this->getRequest()->getValueNames();
00930 
00931         if ( !$this->mInternalMode ) {
00932             // Printer has not yet executed; don't warn that its parameters are unused
00933             $printerParams = array_map(
00934                 array( $this->mPrinter, 'encodeParamName' ),
00935                 array_keys( $this->mPrinter->getFinalParams() ?: array() )
00936             );
00937             $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
00938         } else {
00939             $unusedParams = array_diff( $allParams, $paramsUsed );
00940         }
00941 
00942         if ( count( $unusedParams ) ) {
00943             $s = count( $unusedParams ) > 1 ? 's' : '';
00944             $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
00945         }
00946     }
00947 
00953     protected function printResult( $isError ) {
00954         global $wgDebugAPI;
00955         if ( $wgDebugAPI !== false ) {
00956             $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
00957         }
00958 
00959         $this->getResult()->cleanUpUTF8();
00960         $printer = $this->mPrinter;
00961         $printer->profileIn();
00962 
00968         $isHelp = $isError || $this->mAction == 'help';
00969         $printer->setUnescapeAmps( $isHelp && $printer->getFormat() == 'XML' && $printer->getIsHtml() );
00970 
00971         $printer->initPrinter( $isHelp );
00972 
00973         $printer->execute();
00974         $printer->closePrinter();
00975         $printer->profileOut();
00976     }
00977 
00981     public function isReadMode() {
00982         return false;
00983     }
00984 
00990     public function getAllowedParams() {
00991         return array(
00992             'format' => array(
00993                 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
00994                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' )
00995             ),
00996             'action' => array(
00997                 ApiBase::PARAM_DFLT => 'help',
00998                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' )
00999             ),
01000             'maxlag' => array(
01001                 ApiBase::PARAM_TYPE => 'integer'
01002             ),
01003             'smaxage' => array(
01004                 ApiBase::PARAM_TYPE => 'integer',
01005                 ApiBase::PARAM_DFLT => 0
01006             ),
01007             'maxage' => array(
01008                 ApiBase::PARAM_TYPE => 'integer',
01009                 ApiBase::PARAM_DFLT => 0
01010             ),
01011             'requestid' => null,
01012             'servedby' => false,
01013             'origin' => null,
01014         );
01015     }
01016 
01022     public function getParamDescription() {
01023         return array(
01024             'format' => 'The format of the output',
01025             'action' => 'What action you would like to perform. See below for module help',
01026             'maxlag' => array(
01027                 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
01028                 'To save actions causing any more site replication lag, this parameter can make the client',
01029                 'wait until the replication lag is less than the specified value.',
01030                 'In case of a replag error, error code "maxlag" is returned, with the message like',
01031                 '"Waiting for $host: $lag seconds lagged\n".',
01032                 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
01033             ),
01034             'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
01035             'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
01036             'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
01037             'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error',
01038             'origin' => array(
01039                 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.',
01040                 'This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).',
01041                 'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .',
01042                 'If this parameter does not match the Origin: header, a 403 response will be returned.',
01043                 'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.',
01044             ),
01045         );
01046     }
01047 
01053     public function getDescription() {
01054         return array(
01055             '',
01056             '',
01057             '**********************************************************************************************************',
01058             '**                                                                                                      **',
01059             '**                      This is an auto-generated MediaWiki API documentation page                      **',
01060             '**                                                                                                      **',
01061             '**                                     Documentation and Examples:                                      **',
01062             '**                                  https://www.mediawiki.org/wiki/API                                  **',
01063             '**                                                                                                      **',
01064             '**********************************************************************************************************',
01065             '',
01066             'Status:                All features shown on this page should be working, but the API',
01067             '                       is still in active development, and may change at any time.',
01068             '                       Make sure to monitor our mailing list for any updates',
01069             '',
01070             'Erroneous requests:    When erroneous requests are sent to the API, a HTTP header will be sent',
01071             '                       with the key "MediaWiki-API-Error" and then both the value of the',
01072             '                       header and the error code sent back will be set to the same value',
01073             '',
01074             '                       In the case of an invalid action being passed, these will have a value',
01075             '                       of "unknown_action"',
01076             '',
01077             '                       For more information see https://www.mediawiki.org/wiki/API:Errors_and_warnings',
01078             '',
01079             'Documentation:         https://www.mediawiki.org/wiki/API:Main_page',
01080             'FAQ                    https://www.mediawiki.org/wiki/API:FAQ',
01081             'Mailing list:          https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
01082             'Api Announcements:     https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
01083             'Bugs & Requests:       https://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
01084             '',
01085             '',
01086             '',
01087             '',
01088             '',
01089         );
01090     }
01091 
01095     public function getPossibleErrors() {
01096         return array_merge( parent::getPossibleErrors(), array(
01097             array( 'readonlytext' ),
01098             array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ),
01099             array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ),
01100             array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ),
01101             array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ),
01102         ) );
01103     }
01104 
01109     protected function getCredits() {
01110         return array(
01111             'API developers:',
01112             '    Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-2009)',
01113             '    Victor Vasiliev - vasilvv @ gmail . com',
01114             '    Bryan Tong Minh - bryan . tongminh @ gmail . com',
01115             '    Sam Reed - sam @ reedyboy . net',
01116             '    Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007, 2012-present)',
01117             '',
01118             'Please send your comments, suggestions and questions to [email protected]',
01119             'or file a bug report at https://bugzilla.wikimedia.org/'
01120         );
01121     }
01122 
01128     public function setHelp( $help = true ) {
01129         $this->mPrinter->setHelp( $help );
01130     }
01131 
01137     public function makeHelpMsg() {
01138         global $wgMemc, $wgAPICacheHelpTimeout;
01139         $this->setHelp();
01140         // Get help text from cache if present
01141         $key = wfMemcKey( 'apihelp', $this->getModuleName(),
01142             str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
01143         if ( $wgAPICacheHelpTimeout > 0 ) {
01144             $cached = $wgMemc->get( $key );
01145             if ( $cached ) {
01146                 return $cached;
01147             }
01148         }
01149         $retval = $this->reallyMakeHelpMsg();
01150         if ( $wgAPICacheHelpTimeout > 0 ) {
01151             $wgMemc->set( $key, $retval, $wgAPICacheHelpTimeout );
01152         }
01153         return $retval;
01154     }
01155 
01159     public function reallyMakeHelpMsg() {
01160         $this->setHelp();
01161 
01162         // Use parent to make default message for the main module
01163         $msg = parent::makeHelpMsg();
01164 
01165         $astriks = str_repeat( '*** ', 14 );
01166         $msg .= "\n\n$astriks Modules  $astriks\n\n";
01167 
01168         foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
01169             $module = $this->mModuleMgr->getModule( $name );
01170             $msg .= self::makeHelpMsgHeader( $module, 'action' );
01171 
01172             $msg2 = $module->makeHelpMsg();
01173             if ( $msg2 !== false ) {
01174                 $msg .= $msg2;
01175             }
01176             $msg .= "\n";
01177         }
01178 
01179         $msg .= "\n$astriks Permissions $astriks\n\n";
01180         foreach ( self::$mRights as $right => $rightMsg ) {
01181             $groups = User::getGroupsWithPermission( $right );
01182             $msg .= "* " . $right . " *\n  " . wfMsgReplaceArgs( $rightMsg['msg'], $rightMsg['params'] ) .
01183                         "\nGranted to:\n  " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
01184         }
01185 
01186         $msg .= "\n$astriks Formats  $astriks\n\n";
01187         foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
01188             $module = $this->mModuleMgr->getModule( $name );
01189             $msg .= self::makeHelpMsgHeader( $module, 'format' );
01190             $msg2 = $module->makeHelpMsg();
01191             if ( $msg2 !== false ) {
01192                 $msg .= $msg2;
01193             }
01194             $msg .= "\n";
01195         }
01196 
01197         $msg .= "\n*** Credits: ***\n   " . implode( "\n   ", $this->getCredits() ) . "\n";
01198 
01199         return $msg;
01200     }
01201 
01207     public static function makeHelpMsgHeader( $module, $paramName ) {
01208         $modulePrefix = $module->getModulePrefix();
01209         if ( strval( $modulePrefix ) !== '' ) {
01210             $modulePrefix = "($modulePrefix) ";
01211         }
01212 
01213         return "* $paramName={$module->getModuleName()} $modulePrefix*";
01214     }
01215 
01216     private $mCanApiHighLimits = null;
01217 
01222     public function canApiHighLimits() {
01223         if ( !isset( $this->mCanApiHighLimits ) ) {
01224             $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
01225         }
01226 
01227         return $this->mCanApiHighLimits;
01228     }
01229 
01235     public function getShowVersions() {
01236         wfDeprecated( __METHOD__, '1.21' );
01237         return false;
01238     }
01239 
01244     public function getModuleManager() {
01245         return $this->mModuleMgr;
01246     }
01247 
01257     protected function addModule( $name, $class ) {
01258         $this->getModuleManager()->addModule( $name, 'action', $class );
01259     }
01260 
01269     protected function addFormat( $name, $class ) {
01270         $this->getModuleManager()->addModule( $name, 'format', $class );
01271     }
01272 
01278     function getModules() {
01279         return $this->getModuleManager()->getNamesWithClasses( 'action' );
01280     }
01281 
01289     public function getFormats() {
01290         return $this->getModuleManager()->getNamesWithClasses( 'format' );
01291     }
01292 }
01293 
01300 class UsageException extends MWException {
01301 
01302     private $mCodestr;
01303 
01307     private $mExtraData;
01308 
01315     public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
01316         parent::__construct( $message, $code );
01317         $this->mCodestr = $codestr;
01318         $this->mExtraData = $extradata;
01319     }
01320 
01324     public function getCodeString() {
01325         return $this->mCodestr;
01326     }
01327 
01331     public function getMessageArray() {
01332         $result = array(
01333             'code' => $this->mCodestr,
01334             'info' => $this->getMessage()
01335         );
01336         if ( is_array( $this->mExtraData ) ) {
01337             $result = array_merge( $result, $this->mExtraData );
01338         }
01339         return $result;
01340     }
01341 
01345     public function __toString() {
01346         return "{$this->getCodeString()}: {$this->getMessage()}";
01347     }
01348 }