MediaWiki  REL1_24
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         '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 }