MediaWiki  REL1_23
ApiMain.php
Go to the documentation of this file.
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 }