MediaWiki  REL1_21
ApiMain.php
Go to the documentation of this file.
00001 <?php
00041 class ApiMain extends ApiBase {
00042 
00046         const API_DEFAULT_FORMAT = 'xmlfm';
00047 
00051         private static $Modules = array(
00052                 'login' => 'ApiLogin',
00053                 'logout' => 'ApiLogout',
00054                 'createaccount' => 'ApiCreateAccount',
00055                 'query' => 'ApiQuery',
00056                 'expandtemplates' => 'ApiExpandTemplates',
00057                 'parse' => 'ApiParse',
00058                 'opensearch' => 'ApiOpenSearch',
00059                 'feedcontributions' => 'ApiFeedContributions',
00060                 'feedwatchlist' => 'ApiFeedWatchlist',
00061                 'help' => 'ApiHelp',
00062                 'paraminfo' => 'ApiParamInfo',
00063                 'rsd' => 'ApiRsd',
00064                 'compare' => 'ApiComparePages',
00065                 'tokens' => 'ApiTokens',
00066 
00067                 // Write modules
00068                 'purge' => 'ApiPurge',
00069                 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
00070                 'rollback' => 'ApiRollback',
00071                 'delete' => 'ApiDelete',
00072                 'undelete' => 'ApiUndelete',
00073                 'protect' => 'ApiProtect',
00074                 'block' => 'ApiBlock',
00075                 'unblock' => 'ApiUnblock',
00076                 'move' => 'ApiMove',
00077                 'edit' => 'ApiEditPage',
00078                 'upload' => 'ApiUpload',
00079                 'filerevert' => 'ApiFileRevert',
00080                 'emailuser' => 'ApiEmailUser',
00081                 'watch' => 'ApiWatch',
00082                 'patrol' => 'ApiPatrol',
00083                 'import' => 'ApiImport',
00084                 'userrights' => 'ApiUserrights',
00085                 'options' => 'ApiOptions',
00086                 'imagerotate' =>'ApiImageRotate',
00087         );
00088 
00092         private static $Formats = array(
00093                 'json' => 'ApiFormatJson',
00094                 'jsonfm' => 'ApiFormatJson',
00095                 'php' => 'ApiFormatPhp',
00096                 'phpfm' => 'ApiFormatPhp',
00097                 'wddx' => 'ApiFormatWddx',
00098                 'wddxfm' => 'ApiFormatWddx',
00099                 'xml' => 'ApiFormatXml',
00100                 'xmlfm' => 'ApiFormatXml',
00101                 'yaml' => 'ApiFormatYaml',
00102                 'yamlfm' => 'ApiFormatYaml',
00103                 'rawfm' => 'ApiFormatJson',
00104                 'txt' => 'ApiFormatTxt',
00105                 'txtfm' => 'ApiFormatTxt',
00106                 'dbg' => 'ApiFormatDbg',
00107                 'dbgfm' => 'ApiFormatDbg',
00108                 'dump' => 'ApiFormatDump',
00109                 'dumpfm' => 'ApiFormatDump',
00110                 'none' => 'ApiFormatNone',
00111         );
00112 
00119         private static $mRights = array(
00120                 'writeapi' => array(
00121                         'msg' => 'Use of the write API',
00122                         'params' => array()
00123                 ),
00124                 'apihighlimits' => array(
00125                         'msg' => 'Use higher limits in API queries (Slow queries: $1 results; Fast queries: $2 results). The limits for slow queries also apply to multivalue parameters.',
00126                         'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 )
00127                 )
00128         );
00129 
00133         private $mPrinter;
00134 
00135         private $mModuleMgr, $mResult;
00136         private $mAction;
00137         private $mEnableWrite;
00138         private $mInternalMode, $mSquidMaxage, $mModule;
00139 
00140         private $mCacheMode = 'private';
00141         private $mCacheControl = array();
00142         private $mParamsUsed = array();
00143 
00150         public function __construct( $context = null, $enableWrite = false ) {
00151                 if ( $context === null ) {
00152                         $context = RequestContext::getMain();
00153                 } elseif ( $context instanceof WebRequest ) {
00154                         // BC for pre-1.19
00155                         $request = $context;
00156                         $context = RequestContext::getMain();
00157                 }
00158                 // We set a derivative context so we can change stuff later
00159                 $this->setContext( new DerivativeContext( $context ) );
00160 
00161                 if ( isset( $request ) ) {
00162                         $this->getContext()->setRequest( $request );
00163                 }
00164 
00165                 $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
00166 
00167                 // Special handling for the main module: $parent === $this
00168                 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
00169 
00170                 if ( !$this->mInternalMode ) {
00171                         // Impose module restrictions.
00172                         // If the current user cannot read,
00173                         // Remove all modules other than login
00174                         global $wgUser;
00175 
00176                         if ( $this->getVal( 'callback' ) !== null ) {
00177                                 // JSON callback allows cross-site reads.
00178                                 // For safety, strip user credentials.
00179                                 wfDebug( "API: stripping user credentials for JSON callback\n" );
00180                                 $wgUser = new User();
00181                                 $this->getContext()->setUser( $wgUser );
00182                         }
00183                 }
00184 
00185                 global $wgAPIModules;
00186                 $this->mModuleMgr = new ApiModuleManager( $this );
00187                 $this->mModuleMgr->addModules( self::$Modules, 'action' );
00188                 $this->mModuleMgr->addModules( $wgAPIModules, 'action' );
00189                 $this->mModuleMgr->addModules( self::$Formats, 'format' );
00190 
00191                 $this->mResult = new ApiResult( $this );
00192                 $this->mEnableWrite = $enableWrite;
00193 
00194                 $this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling()
00195                 $this->mCommit = false;
00196         }
00197 
00202         public function isInternalMode() {
00203                 return $this->mInternalMode;
00204         }
00205 
00211         public function getResult() {
00212                 return $this->mResult;
00213         }
00214 
00220         public function getModule() {
00221                 return $this->mModule;
00222         }
00223 
00229         public function getPrinter() {
00230                 return $this->mPrinter;
00231         }
00232 
00238         public function setCacheMaxAge( $maxage ) {
00239                 $this->setCacheControl( array(
00240                         'max-age' => $maxage,
00241                         's-maxage' => $maxage
00242                 ) );
00243         }
00244 
00270         public function setCacheMode( $mode ) {
00271                 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) {
00272                         wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
00273                         // Ignore for forwards-compatibility
00274                         return;
00275                 }
00276 
00277                 if ( !User::groupHasPermission( '*', 'read' ) ) {
00278                         // Private wiki, only private headers
00279                         if ( $mode !== 'private' ) {
00280                                 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
00281                                 return;
00282                         }
00283                 }
00284 
00285                 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
00286                 $this->mCacheMode = $mode;
00287         }
00288 
00294         public function setCachePrivate() {
00295                 wfDeprecated( __METHOD__, '1.17' );
00296                 $this->setCacheMode( 'private' );
00297         }
00298 
00309         public function setCacheControl( $directives ) {
00310                 $this->mCacheControl = $directives + $this->mCacheControl;
00311         }
00312 
00323         public function setVaryCookie() {
00324                 wfDeprecated( __METHOD__, '1.17' );
00325                 $this->setCacheMode( 'anon-public-user-private' );
00326         }
00327 
00335         public function createPrinterByName( $format ) {
00336                 $printer = $this->mModuleMgr->getModule( $format, 'format' );
00337                 if ( $printer === null ) {
00338                         $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
00339                 }
00340                 return $printer;
00341         }
00342 
00346         public function execute() {
00347                 $this->profileIn();
00348                 if ( $this->mInternalMode ) {
00349                         $this->executeAction();
00350                 } else {
00351                         $this->executeActionWithErrorHandling();
00352                 }
00353 
00354                 $this->profileOut();
00355         }
00356 
00361         protected function executeActionWithErrorHandling() {
00362                 // Verify the CORS header before executing the action
00363                 if ( !$this->handleCORS() ) {
00364                         // handleCORS() has sent a 403, abort
00365                         return;
00366                 }
00367 
00368                 // Exit here if the request method was OPTIONS
00369                 // (assume there will be a followup GET or POST)
00370                 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
00371                         return;
00372                 }
00373 
00374                 // In case an error occurs during data output,
00375                 // clear the output buffer and print just the error information
00376                 ob_start();
00377 
00378                 $t = microtime( true );
00379                 try {
00380                         $this->executeAction();
00381                 } catch ( Exception $e ) {
00382                         // Allow extra cleanup and logging
00383                         wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
00384 
00385                         // Log it
00386                         if ( $e instanceof MWException && !( $e instanceof UsageException ) ) {
00387                                 global $wgLogExceptionBacktrace;
00388                                 if ( $wgLogExceptionBacktrace ) {
00389                                         wfDebugLog( 'exception', $e->getLogMessage() . "\n" . $e->getTraceAsString() . "\n" );
00390                                 } else {
00391                                         wfDebugLog( 'exception', $e->getLogMessage() );
00392                                 }
00393                         }
00394 
00395                         // Handle any kind of exception by outputting properly formatted error message.
00396                         // If this fails, an unhandled exception should be thrown so that global error
00397                         // handler will process and log it.
00398 
00399                         $errCode = $this->substituteResultWithError( $e );
00400 
00401                         // Error results should not be cached
00402                         $this->setCacheMode( 'private' );
00403 
00404                         $response = $this->getRequest()->response();
00405                         $headerStr = 'MediaWiki-API-Error: ' . $errCode;
00406                         if ( $e->getCode() === 0 ) {
00407                                 $response->header( $headerStr );
00408                         } else {
00409                                 $response->header( $headerStr, true, $e->getCode() );
00410                         }
00411 
00412                         // Reset and print just the error message
00413                         ob_clean();
00414 
00415                         // If the error occurred during printing, do a printer->profileOut()
00416                         $this->mPrinter->safeProfileOut();
00417                         $this->printResult( true );
00418                 }
00419 
00420                 // Log the request whether or not there was an error
00421                 $this->logRequest( microtime( true ) - $t);
00422 
00423                 // Send cache headers after any code which might generate an error, to
00424                 // avoid sending public cache headers for errors.
00425                 $this->sendCacheHeaders();
00426 
00427                 if ( $this->mPrinter->getIsHtml() && !$this->mPrinter->isDisabled() ) {
00428                         echo wfReportTime();
00429                 }
00430 
00431                 ob_end_flush();
00432         }
00433 
00446         protected function handleCORS() {
00447                 global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions;
00448 
00449                 $originParam = $this->getParameter( 'origin' ); // defaults to null
00450                 if ( $originParam === null ) {
00451                         // No origin parameter, nothing to do
00452                         return true;
00453                 }
00454 
00455                 $request = $this->getRequest();
00456                 $response = $request->response();
00457                 // Origin: header is a space-separated list of origins, check all of them
00458                 $originHeader = $request->getHeader( 'Origin' );
00459                 if ( $originHeader === false ) {
00460                         $origins = array();
00461                 } else {
00462                         $origins = explode( ' ', $originHeader );
00463                 }
00464                 if ( !in_array( $originParam, $origins ) ) {
00465                         // origin parameter set but incorrect
00466                         // Send a 403 response
00467                         $message = HttpStatus::getMessage( 403 );
00468                         $response->header( "HTTP/1.1 403 $message", true, 403 );
00469                         $response->header( 'Cache-Control: no-cache' );
00470                         echo "'origin' parameter does not match Origin header\n";
00471                         return false;
00472                 }
00473                 if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) {
00474                         $response->header( "Access-Control-Allow-Origin: $originParam" );
00475                         $response->header( 'Access-Control-Allow-Credentials: true' );
00476                         $this->getOutput()->addVaryHeader( 'Origin' );
00477                 }
00478                 return true;
00479         }
00480 
00488         protected static function matchOrigin( $value, $rules, $exceptions ) {
00489                 foreach ( $rules as $rule ) {
00490                         if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
00491                                 // Rule matches, check exceptions
00492                                 foreach ( $exceptions as $exc ) {
00493                                         if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
00494                                                 return false;
00495                                         }
00496                                 }
00497                                 return true;
00498                         }
00499                 }
00500                 return false;
00501         }
00502 
00511         protected static function wildcardToRegex( $wildcard ) {
00512                 $wildcard = preg_quote( $wildcard, '/' );
00513                 $wildcard = str_replace(
00514                         array( '\*', '\?' ),
00515                         array( '.*?', '.' ),
00516                         $wildcard
00517                 );
00518                 return "/https?:\/\/$wildcard/";
00519         }
00520 
00521         protected function sendCacheHeaders() {
00522                 global $wgUseXVO, $wgVaryOnXFP;
00523                 $response = $this->getRequest()->response();
00524                 $out = $this->getOutput();
00525 
00526                 if ( $wgVaryOnXFP ) {
00527                         $out->addVaryHeader( 'X-Forwarded-Proto' );
00528                 }
00529 
00530                 if ( $this->mCacheMode == 'private' ) {
00531                         $response->header( 'Cache-Control: private' );
00532                         return;
00533                 }
00534 
00535                 if ( $this->mCacheMode == 'anon-public-user-private' ) {
00536                         $out->addVaryHeader( 'Cookie' );
00537                         $response->header( $out->getVaryHeader() );
00538                         if ( $wgUseXVO ) {
00539                                 $response->header( $out->getXVO() );
00540                                 if ( $out->haveCacheVaryCookies() ) {
00541                                         // Logged in, mark this request private
00542                                         $response->header( 'Cache-Control: private' );
00543                                         return;
00544                                 }
00545                                 // Logged out, send normal public headers below
00546                         } elseif ( session_id() != '' ) {
00547                                 // Logged in or otherwise has session (e.g. anonymous users who have edited)
00548                                 // Mark request private
00549                                 $response->header( 'Cache-Control: private' );
00550                                 return;
00551                         } // else no XVO and anonymous, send public headers below
00552                 }
00553 
00554                 // Send public headers
00555                 $response->header( $out->getVaryHeader() );
00556                 if ( $wgUseXVO ) {
00557                         $response->header( $out->getXVO() );
00558                 }
00559 
00560                 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
00561                 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
00562                         $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
00563                 }
00564                 if ( !isset( $this->mCacheControl['max-age'] ) ) {
00565                         $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
00566                 }
00567 
00568                 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
00569                         // Public cache not requested
00570                         // Sending a Vary header in this case is harmless, and protects us
00571                         // against conditional calls of setCacheMaxAge().
00572                         $response->header( 'Cache-Control: private' );
00573                         return;
00574                 }
00575 
00576                 $this->mCacheControl['public'] = true;
00577 
00578                 // Send an Expires header
00579                 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
00580                 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
00581                 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
00582 
00583                 // Construct the Cache-Control header
00584                 $ccHeader = '';
00585                 $separator = '';
00586                 foreach ( $this->mCacheControl as $name => $value ) {
00587                         if ( is_bool( $value ) ) {
00588                                 if ( $value ) {
00589                                         $ccHeader .= $separator . $name;
00590                                         $separator = ', ';
00591                                 }
00592                         } else {
00593                                 $ccHeader .= $separator . "$name=$value";
00594                                 $separator = ', ';
00595                         }
00596                 }
00597 
00598                 $response->header( "Cache-Control: $ccHeader" );
00599         }
00600 
00607         protected function substituteResultWithError( $e ) {
00608                 global $wgShowHostnames;
00609 
00610                 $result = $this->getResult();
00611                 // Printer may not be initialized if the extractRequestParams() fails for the main module
00612                 if ( !isset ( $this->mPrinter ) ) {
00613                         // The printer has not been created yet. Try to manually get formatter value.
00614                         $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
00615                         if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
00616                                 $value = self::API_DEFAULT_FORMAT;
00617                         }
00618 
00619                         $this->mPrinter = $this->createPrinterByName( $value );
00620                         if ( $this->mPrinter->getNeedsRawData() ) {
00621                                 $result->setRawMode();
00622                         }
00623                 }
00624 
00625                 if ( $e instanceof UsageException ) {
00626                         // User entered incorrect parameters - print usage screen
00627                         $errMessage = $e->getMessageArray();
00628 
00629                         // Only print the help message when this is for the developer, not runtime
00630                         if ( $this->mPrinter->getWantsHelp() || $this->mAction == 'help' ) {
00631                                 ApiResult::setContent( $errMessage, $this->makeHelpMsg() );
00632                         }
00633                 } else {
00634                         global $wgShowSQLErrors, $wgShowExceptionDetails;
00635                         // Something is seriously wrong
00636                         if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) {
00637                                 $info = 'Database query error';
00638                         } else {
00639                                 $info = "Exception Caught: {$e->getMessage()}";
00640                         }
00641 
00642                         $errMessage = array(
00643                                 'code' => 'internal_api_error_' . get_class( $e ),
00644                                 'info' => $info,
00645                         );
00646                         ApiResult::setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' );
00647                 }
00648 
00649                 // Remember all the warnings to re-add them later
00650                 $oldResult = $result->getData();
00651                 $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
00652 
00653                 $result->reset();
00654                 $result->disableSizeCheck();
00655                 // Re-add the id
00656                 $requestid = $this->getParameter( 'requestid' );
00657                 if ( !is_null( $requestid ) ) {
00658                         $result->addValue( null, 'requestid', $requestid );
00659                 }
00660                 if ( $wgShowHostnames ) {
00661                         // servedby is especially useful when debugging errors
00662                         $result->addValue( null, 'servedby', wfHostName() );
00663                 }
00664                 if ( $warnings !== null ) {
00665                         $result->addValue( null, 'warnings', $warnings );
00666                 }
00667 
00668                 $result->addValue( null, 'error', $errMessage );
00669 
00670                 return $errMessage['code'];
00671         }
00672 
00677         protected function setupExecuteAction() {
00678                 global $wgShowHostnames;
00679 
00680                 // First add the id to the top element
00681                 $result = $this->getResult();
00682                 $requestid = $this->getParameter( 'requestid' );
00683                 if ( !is_null( $requestid ) ) {
00684                         $result->addValue( null, 'requestid', $requestid );
00685                 }
00686 
00687                 if ( $wgShowHostnames ) {
00688                         $servedby = $this->getParameter( 'servedby' );
00689                         if ( $servedby ) {
00690                                 $result->addValue( null, 'servedby', wfHostName() );
00691                         }
00692                 }
00693 
00694                 $params = $this->extractRequestParams();
00695 
00696                 $this->mAction = $params['action'];
00697 
00698                 if ( !is_string( $this->mAction ) ) {
00699                         $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
00700                 }
00701 
00702                 return $params;
00703         }
00704 
00709         protected function setupModule() {
00710                 // Instantiate the module requested by the user
00711                 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
00712                 if ( $module === null ) {
00713                         $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
00714                 }
00715                 $moduleParams = $module->extractRequestParams();
00716 
00717                 // Die if token required, but not provided
00718                 $salt = $module->getTokenSalt();
00719                 if ( $salt !== false ) {
00720                         if ( !isset( $moduleParams['token'] ) ) {
00721                                 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
00722                         } else {
00723                                 if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getContext()->getRequest() ) ) {
00724                                         $this->dieUsageMsg( 'sessionfailure' );
00725                                 }
00726                         }
00727                 }
00728                 return $module;
00729         }
00730 
00737         protected function checkMaxLag( $module, $params ) {
00738                 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
00739                         // Check for maxlag
00740                         global $wgShowHostnames;
00741                         $maxLag = $params['maxlag'];
00742                         list( $host, $lag ) = wfGetLB()->getMaxLag();
00743                         if ( $lag > $maxLag ) {
00744                                 $response = $this->getRequest()->response();
00745 
00746                                 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
00747                                 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
00748 
00749                                 if ( $wgShowHostnames ) {
00750                                         $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
00751                                 } else {
00752                                         $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
00753                                 }
00754                                 return false;
00755                         }
00756                 }
00757                 return true;
00758         }
00759 
00764         protected function checkExecutePermissions( $module ) {
00765                 $user = $this->getUser();
00766                 if ( $module->isReadMode() && !User::groupHasPermission( '*', 'read' ) &&
00767                         !$user->isAllowed( 'read' ) )
00768                 {
00769                         $this->dieUsageMsg( 'readrequired' );
00770                 }
00771                 if ( $module->isWriteMode() ) {
00772                         if ( !$this->mEnableWrite ) {
00773                                 $this->dieUsageMsg( 'writedisabled' );
00774                         }
00775                         if ( !$user->isAllowed( 'writeapi' ) ) {
00776                                 $this->dieUsageMsg( 'writerequired' );
00777                         }
00778                         if ( wfReadOnly() ) {
00779                                 $this->dieReadOnly();
00780                         }
00781                 }
00782 
00783                 // Allow extensions to stop execution for arbitrary reasons.
00784                 $message = false;
00785                 if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
00786                         $this->dieUsageMsg( $message );
00787                 }
00788         }
00789 
00795         protected function setupExternalResponse( $module, $params ) {
00796                 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
00797                         // Module requires POST. GET request might still be allowed
00798                         // if $wgDebugApi is true, otherwise fail.
00799                         $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) );
00800                 }
00801 
00802                 // See if custom printer is used
00803                 $this->mPrinter = $module->getCustomPrinter();
00804                 if ( is_null( $this->mPrinter ) ) {
00805                         // Create an appropriate printer
00806                         $this->mPrinter = $this->createPrinterByName( $params['format'] );
00807                 }
00808 
00809                 if ( $this->mPrinter->getNeedsRawData() ) {
00810                         $this->getResult()->setRawMode();
00811                 }
00812         }
00813 
00817         protected function executeAction() {
00818                 $params = $this->setupExecuteAction();
00819                 $module = $this->setupModule();
00820                 $this->mModule = $module;
00821 
00822                 $this->checkExecutePermissions( $module );
00823 
00824                 if ( !$this->checkMaxLag( $module, $params ) ) {
00825                         return;
00826                 }
00827 
00828                 if ( !$this->mInternalMode ) {
00829                         $this->setupExternalResponse( $module, $params );
00830                 }
00831 
00832                 // Execute
00833                 $module->profileIn();
00834                 $module->execute();
00835                 wfRunHooks( 'APIAfterExecute', array( &$module ) );
00836                 $module->profileOut();
00837 
00838                 $this->reportUnusedParams();
00839 
00840                 if ( !$this->mInternalMode ) {
00841                         //append Debug information
00842                         MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
00843 
00844                         // Print result data
00845                         $this->printResult( false );
00846                 }
00847         }
00848 
00853         protected function logRequest( $time ) {
00854                 $request = $this->getRequest();
00855                 $milliseconds = $time === null ? '?' : round( $time * 1000 );
00856                 $s = 'API' .
00857                         ' ' . $request->getMethod() .
00858                         ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
00859                         ' ' . $request->getIP() .
00860                         ' T=' . $milliseconds .'ms';
00861                 foreach ( $this->getParamsUsed() as $name ) {
00862                         $value = $request->getVal( $name );
00863                         if ( $value === null ) {
00864                                 continue;
00865                         }
00866                         $s .= ' ' . $name . '=';
00867                         if ( strlen( $value ) > 256 ) {
00868                                 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
00869                                 $s .= $encValue . '[...]';
00870                         } else {
00871                                 $s .= $this->encodeRequestLogValue( $value );
00872                         }
00873                 }
00874                 $s .= "\n";
00875                 wfDebugLog( 'api', $s, false );
00876         }
00877 
00881         protected function encodeRequestLogValue( $s ) {
00882                 static $table;
00883                 if ( !$table ) {
00884                         $chars = ';@$!*(),/:';
00885                         for ( $i = 0; $i < strlen( $chars ); $i++ ) {
00886                                 $table[rawurlencode( $chars[$i] )] = $chars[$i];
00887                         }
00888                 }
00889                 return strtr( rawurlencode( $s ), $table );
00890         }
00891 
00895         protected function getParamsUsed() {
00896                 return array_keys( $this->mParamsUsed );
00897         }
00898 
00902         public function getVal( $name, $default = null ) {
00903                 $this->mParamsUsed[$name] = true;
00904                 return $this->getRequest()->getVal( $name, $default );
00905         }
00906 
00911         public function getCheck( $name ) {
00912                 $this->mParamsUsed[$name] = true;
00913                 return $this->getRequest()->getCheck( $name );
00914         }
00915 
00923         public function getUpload( $name ) {
00924                 $this->mParamsUsed[$name] = true;
00925                 return $this->getRequest()->getUpload( $name );
00926         }
00927 
00932         protected function reportUnusedParams() {
00933                 $paramsUsed = $this->getParamsUsed();
00934                 $allParams = $this->getRequest()->getValueNames();
00935 
00936                 if ( !$this->mInternalMode ) {
00937                         // Printer has not yet executed; don't warn that its parameters are unused
00938                         $printerParams = array_map(
00939                                 array( $this->mPrinter, 'encodeParamName' ),
00940                                 array_keys( $this->mPrinter->getFinalParams() ?: array() )
00941                         );
00942                         $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
00943                 } else {
00944                         $unusedParams = array_diff( $allParams, $paramsUsed );
00945                 }
00946 
00947                 if( count( $unusedParams ) ) {
00948                         $s = count( $unusedParams ) > 1 ? 's' : '';
00949                         $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
00950                 }
00951         }
00952 
00958         protected function printResult( $isError ) {
00959                 global $wgDebugAPI;
00960                 if( $wgDebugAPI !== false ) {
00961                         $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
00962                 }
00963 
00964                 $this->getResult()->cleanUpUTF8();
00965                 $printer = $this->mPrinter;
00966                 $printer->profileIn();
00967 
00973                 $isHelp = $isError || $this->mAction == 'help';
00974                 $printer->setUnescapeAmps( $isHelp && $printer->getFormat() == 'XML' && $printer->getIsHtml() );
00975 
00976                 $printer->initPrinter( $isHelp );
00977 
00978                 $printer->execute();
00979                 $printer->closePrinter();
00980                 $printer->profileOut();
00981         }
00982 
00986         public function isReadMode() {
00987                 return false;
00988         }
00989 
00995         public function getAllowedParams() {
00996                 return array(
00997                         'format' => array(
00998                                 ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
00999                                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' )
01000                         ),
01001                         'action' => array(
01002                                 ApiBase::PARAM_DFLT => 'help',
01003                                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' )
01004                         ),
01005                         'maxlag'  => array(
01006                                 ApiBase::PARAM_TYPE => 'integer'
01007                         ),
01008                         'smaxage' => array(
01009                                 ApiBase::PARAM_TYPE => 'integer',
01010                                 ApiBase::PARAM_DFLT => 0
01011                         ),
01012                         'maxage' => array(
01013                                 ApiBase::PARAM_TYPE => 'integer',
01014                                 ApiBase::PARAM_DFLT => 0
01015                         ),
01016                         'requestid' => null,
01017                         'servedby'  => false,
01018                         'origin' => null,
01019                 );
01020         }
01021 
01027         public function getParamDescription() {
01028                 return array(
01029                         'format' => 'The format of the output',
01030                         'action' => 'What action you would like to perform. See below for module help',
01031                         'maxlag' => array(
01032                                 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
01033                                 'To save actions causing any more site replication lag, this parameter can make the client',
01034                                 'wait until the replication lag is less than the specified value.',
01035                                 'In case of a replag error, error code "maxlag" is returned, with the message like',
01036                                 '"Waiting for $host: $lag seconds lagged\n".',
01037                                 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
01038                         ),
01039                         'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
01040                         'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
01041                         'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
01042                         'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error',
01043                         'origin' => array(
01044                                 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.',
01045                                 'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .',
01046                                 'If this parameter does not match the Origin: header, a 403 response will be returned.',
01047                                 'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.',
01048                         ),
01049                 );
01050         }
01051 
01057         public function getDescription() {
01058                 return array(
01059                         '',
01060                         '',
01061                         '**********************************************************************************************************',
01062                         '**                                                                                                      **',
01063                         '**                      This is an auto-generated MediaWiki API documentation page                      **',
01064                         '**                                                                                                      **',
01065                         '**                                     Documentation and Examples:                                      **',
01066                         '**                                  https://www.mediawiki.org/wiki/API                                  **',
01067                         '**                                                                                                      **',
01068                         '**********************************************************************************************************',
01069                         '',
01070                         'Status:                All features shown on this page should be working, but the API',
01071                         '                       is still in active development, and may change at any time.',
01072                         '                       Make sure to monitor our mailing list for any updates',
01073                         '',
01074                         'Erroneous requests:    When erroneous requests are sent to the API, a HTTP header will be sent',
01075                         '                       with the key "MediaWiki-API-Error" and then both the value of the',
01076                         '                       header and the error code sent back will be set to the same value',
01077                         '',
01078                         '                       In the case of an invalid action being passed, these will have a value',
01079                         '                       of "unknown_action"',
01080                         '',
01081                         '                       For more information see https://www.mediawiki.org/wiki/API:Errors_and_warnings',
01082                         '',
01083                         'Documentation:         https://www.mediawiki.org/wiki/API:Main_page',
01084                         'FAQ                    https://www.mediawiki.org/wiki/API:FAQ',
01085                         'Mailing list:          https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
01086                         'Api Announcements:     https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
01087                         'Bugs & Requests:       https://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
01088                         '',
01089                         '',
01090                         '',
01091                         '',
01092                         '',
01093                 );
01094         }
01095 
01099         public function getPossibleErrors() {
01100                 return array_merge( parent::getPossibleErrors(), array(
01101                         array( 'readonlytext' ),
01102                         array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ),
01103                         array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ),
01104                         array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ),
01105                         array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ),
01106                 ) );
01107         }
01108 
01113         protected function getCredits() {
01114                 return array(
01115                         'API developers:',
01116                         '    Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-2009)',
01117                         '    Victor Vasiliev - vasilvv at gee mail dot com',
01118                         '    Bryan Tong Minh - bryan . tongminh @ gmail . com',
01119                         '    Sam Reed - sam @ reedyboy . net',
01120                         '    Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007, 2012-present)',
01121                         '',
01122                         'Please send your comments, suggestions and questions to [email protected]',
01123                         'or file a bug report at https://bugzilla.wikimedia.org/'
01124                 );
01125         }
01126 
01132         public function setHelp( $help = true ) {
01133                 $this->mPrinter->setHelp( $help );
01134         }
01135 
01141         public function makeHelpMsg() {
01142                 global $wgMemc, $wgAPICacheHelpTimeout;
01143                 $this->setHelp();
01144                 // Get help text from cache if present
01145                 $key = wfMemcKey( 'apihelp', $this->getModuleName(),
01146                         SpecialVersion::getVersion( 'nodb' ) );
01147                 if ( $wgAPICacheHelpTimeout > 0 ) {
01148                         $cached = $wgMemc->get( $key );
01149                         if ( $cached ) {
01150                                 return $cached;
01151                         }
01152                 }
01153                 $retval = $this->reallyMakeHelpMsg();
01154                 if ( $wgAPICacheHelpTimeout > 0 ) {
01155                         $wgMemc->set( $key, $retval, $wgAPICacheHelpTimeout );
01156                 }
01157                 return $retval;
01158         }
01159 
01163         public function reallyMakeHelpMsg() {
01164                 $this->setHelp();
01165 
01166                 // Use parent to make default message for the main module
01167                 $msg = parent::makeHelpMsg();
01168 
01169                 $astriks = str_repeat( '*** ', 14 );
01170                 $msg .= "\n\n$astriks Modules  $astriks\n\n";
01171 
01172                 foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
01173                         $module = $this->mModuleMgr->getModule( $name );
01174                         $msg .= self::makeHelpMsgHeader( $module, 'action' );
01175 
01176                         $msg2 = $module->makeHelpMsg();
01177                         if ( $msg2 !== false ) {
01178                                 $msg .= $msg2;
01179                         }
01180                         $msg .= "\n";
01181                 }
01182 
01183                 $msg .= "\n$astriks Permissions $astriks\n\n";
01184                 foreach ( self::$mRights as $right => $rightMsg ) {
01185                         $groups = User::getGroupsWithPermission( $right );
01186                         $msg .= "* " . $right . " *\n  " . wfMsgReplaceArgs( $rightMsg['msg'], $rightMsg['params'] ) .
01187                                                 "\nGranted to:\n  " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
01188                 }
01189 
01190                 $msg .= "\n$astriks Formats  $astriks\n\n";
01191                 foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
01192                         $module = $this->mModuleMgr->getModule( $name );
01193                         $msg .= self::makeHelpMsgHeader( $module, 'format' );
01194                         $msg2 = $module->makeHelpMsg();
01195                         if ( $msg2 !== false ) {
01196                                 $msg .= $msg2;
01197                         }
01198                         $msg .= "\n";
01199                 }
01200 
01201                 $msg .= "\n*** Credits: ***\n   " . implode( "\n   ", $this->getCredits() ) . "\n";
01202 
01203                 return $msg;
01204         }
01205 
01211         public static function makeHelpMsgHeader( $module, $paramName ) {
01212                 $modulePrefix = $module->getModulePrefix();
01213                 if ( strval( $modulePrefix ) !== '' ) {
01214                         $modulePrefix = "($modulePrefix) ";
01215                 }
01216 
01217                 return "* $paramName={$module->getModuleName()} $modulePrefix*";
01218         }
01219 
01220         private $mCanApiHighLimits = null;
01221 
01226         public function canApiHighLimits() {
01227                 if ( !isset( $this->mCanApiHighLimits ) ) {
01228                         $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
01229                 }
01230 
01231                 return $this->mCanApiHighLimits;
01232         }
01233 
01239         public function getShowVersions() {
01240                 wfDeprecated( __METHOD__, '1.21' );
01241                 return false;
01242         }
01243 
01248         public function getModuleManager() {
01249                 return $this->mModuleMgr;
01250         }
01251 
01261         protected function addModule( $name, $class ) {
01262                 $this->getModuleManager()->addModule( $name, 'action', $class );
01263         }
01264 
01273         protected function addFormat( $name, $class ) {
01274                 $this->getModuleManager()->addModule( $name, 'format', $class );
01275         }
01276 
01282         function getModules() {
01283                 return $this->getModuleManager()->getNamesWithClasses( 'action' );
01284         }
01285 
01293         public function getFormats() {
01294                 return $this->getModuleManager()->getNamesWithClasses( 'format' );
01295         }
01296 }
01297 
01304 class UsageException extends MWException {
01305 
01306         private $mCodestr;
01307 
01311         private $mExtraData;
01312 
01319         public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
01320                 parent::__construct( $message, $code );
01321                 $this->mCodestr = $codestr;
01322                 $this->mExtraData = $extradata;
01323         }
01324 
01328         public function getCodeString() {
01329                 return $this->mCodestr;
01330         }
01331 
01335         public function getMessageArray() {
01336                 $result = array(
01337                         'code' => $this->mCodestr,
01338                         'info' => $this->getMessage()
01339                 );
01340                 if ( is_array( $this->mExtraData ) ) {
01341                         $result = array_merge( $result, $this->mExtraData );
01342                 }
01343                 return $result;
01344         }
01345 
01349         public function __toString() {
01350                 return "{$this->getCodeString()}: {$this->getMessage()}";
01351         }
01352 }