diff options
Diffstat (limited to 'includes/api/ApiMain.php')
-rw-r--r-- | includes/api/ApiMain.php | 209 |
1 files changed, 190 insertions, 19 deletions
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 3bf066cf..d943c86b 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -100,8 +100,6 @@ class ApiMain extends ApiBase { 'jsonfm' => 'ApiFormatJson', 'php' => 'ApiFormatPhp', 'phpfm' => 'ApiFormatPhp', - 'wddx' => 'ApiFormatWddx', - 'wddxfm' => 'ApiFormatWddx', 'xml' => 'ApiFormatXml', 'xmlfm' => 'ApiFormatXml', 'yaml' => 'ApiFormatYaml', @@ -111,8 +109,6 @@ class ApiMain extends ApiBase { 'txtfm' => 'ApiFormatTxt', 'dbg' => 'ApiFormatDbg', 'dbgfm' => 'ApiFormatDbg', - 'dump' => 'ApiFormatDump', - 'dumpfm' => 'ApiFormatDump', 'none' => 'ApiFormatNone', ); @@ -429,13 +425,16 @@ class ApiMain extends ApiBase { // In case an error occurs during data output, // clear the output buffer and print just the error information + $obLevel = ob_get_level(); ob_start(); $t = microtime( true ); try { $this->executeAction(); + $isError = false; } catch ( Exception $e ) { $this->handleException( $e ); + $isError = true; } // Log the request whether or not there was an error @@ -443,9 +442,13 @@ class ApiMain extends ApiBase { // Send cache headers after any code which might generate an error, to // avoid sending public cache headers for errors. - $this->sendCacheHeaders(); + $this->sendCacheHeaders( $isError ); - ob_end_flush(); + // Executing the action might have already messed with the output + // buffers. + while ( ob_get_level() > $obLevel ) { + ob_end_flush(); + } } /** @@ -536,7 +539,7 @@ class ApiMain extends ApiBase { // Log the request and reset cache headers $main->logRequest( 0 ); - $main->sendCacheHeaders(); + $main->sendCacheHeaders( true ); ob_end_flush(); } @@ -577,8 +580,7 @@ class ApiMain extends ApiBase { if ( !in_array( $originParam, $origins ) ) { // origin parameter set but incorrect // Send a 403 response - $message = HttpStatus::getMessage( 403 ); - $response->header( "HTTP/1.1 403 $message", true, 403 ); + $response->statusHeader( 403 ); $response->header( 'Cache-Control: no-cache' ); echo "'origin' parameter does not match Origin header\n"; @@ -706,7 +708,12 @@ class ApiMain extends ApiBase { return "/^https?:\/\/$wildcard$/"; } - protected function sendCacheHeaders() { + /** + * Send caching headers + * @param boolean $isError Whether an error response is being output + * @since 1.26 added $isError parameter + */ + protected function sendCacheHeaders( $isError ) { $response = $this->getRequest()->response(); $out = $this->getOutput(); @@ -716,6 +723,19 @@ class ApiMain extends ApiBase { $out->addVaryHeader( 'X-Forwarded-Proto' ); } + if ( !$isError && $this->mModule && + ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' ) + ) { + $etag = $this->mModule->getConditionalRequestData( 'etag' ); + if ( $etag !== null ) { + $response->header( "ETag: $etag" ); + } + $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' ); + if ( $lastMod !== null ) { + $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) ); + } + } + // The logic should be: // $this->mCacheControl['max-age'] is set? // Use it, the module knows better than our guess. @@ -999,6 +1019,121 @@ class ApiMain extends ApiBase { } /** + * Check selected RFC 7232 precondition headers + * + * RFC 7232 envisions a particular model where you send your request to "a + * resource", and for write requests that you can read "the resource" by + * changing the method to GET. When the API receives a GET request, it + * works out even though "the resource" from RFC 7232's perspective might + * be many resources from MediaWiki's perspective. But it totally fails for + * a POST, since what HTTP sees as "the resource" is probably just + * "/api.php" with all the interesting bits in the body. + * + * Therefore, we only support RFC 7232 precondition headers for GET (and + * HEAD). That means we don't need to bother with If-Match and + * If-Unmodified-Since since they only apply to modification requests. + * + * And since we don't support Range, If-Range is ignored too. + * + * @since 1.26 + * @param ApiBase $module Api module being used + * @return bool True on success, false should exit immediately + */ + protected function checkConditionalRequestHeaders( $module ) { + if ( $this->mInternalMode ) { + // No headers to check in internal mode + return true; + } + + if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) { + // Don't check POSTs + return true; + } + + $return304 = false; + + $ifNoneMatch = array_diff( + $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: array(), + array( '' ) + ); + if ( $ifNoneMatch ) { + if ( $ifNoneMatch === array( '*' ) ) { + // API responses always "exist" + $etag = '*'; + } else { + $etag = $module->getConditionalRequestData( 'etag' ); + } + } + if ( $ifNoneMatch && $etag !== null ) { + $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag; + $match = array_map( function ( $s ) { + return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s; + }, $ifNoneMatch ); + $return304 = in_array( $test, $match, true ); + } else { + $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) ); + + // Some old browsers sends sizes after the date, like this: + // Wed, 20 Aug 2003 06:51:19 GMT; length=5202 + // Ignore that. + $i = strpos( $value, ';' ); + if ( $i !== false ) { + $value = trim( substr( $value, 0, $i ) ); + } + + if ( $value !== '' ) { + try { + $ts = new MWTimestamp( $value ); + if ( + // RFC 7231 IMF-fixdate + $ts->getTimestamp( TS_RFC2822 ) === $value || + // RFC 850 + $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value || + // asctime (with and without space-padded day) + $ts->format( 'D M j H:i:s Y' ) === $value || + $ts->format( 'D M j H:i:s Y' ) === $value + ) { + $lastMod = $module->getConditionalRequestData( 'last-modified' ); + if ( $lastMod !== null ) { + // Mix in some MediaWiki modification times + $modifiedTimes = array( + 'page' => $lastMod, + 'user' => $this->getUser()->getTouched(), + 'epoch' => $this->getConfig()->get( 'CacheEpoch' ), + ); + if ( $this->getConfig()->get( 'UseSquid' ) ) { + // T46570: the core page itself may not change, but resources might + $modifiedTimes['sepoch'] = wfTimestamp( + TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' ) + ); + } + Hooks::run( 'OutputPageCheckLastModified', array( &$modifiedTimes ) ); + $lastMod = max( $modifiedTimes ); + $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW ); + } + } + } catch ( TimestampException $e ) { + // Invalid timestamp, ignore it + } + } + } + + if ( $return304 ) { + $this->getRequest()->response()->statusHeader( 304 ); + + // Avoid outputting the compressed representation of a zero-length body + MediaWiki\suppressWarnings(); + ini_set( 'zlib.output_compression', 0 ); + MediaWiki\restoreWarnings(); + wfClearOutputBuffers(); + + return false; + } + + return true; + } + + /** * Check for sufficient permissions to execute * @param ApiBase $module An Api module */ @@ -1068,10 +1203,6 @@ class ApiMain extends ApiBase { // Create an appropriate printer $this->mPrinter = $this->createPrinterByName( $params['format'] ); } - - if ( $this->mPrinter->getNeedsRawData() ) { - $this->getResult()->setRawMode(); - } } /** @@ -1088,6 +1219,10 @@ class ApiMain extends ApiBase { return; } + if ( !$this->checkConditionalRequestHeaders( $module ) ) { + return; + } + if ( !$this->mInternalMode ) { $this->setupExternalResponse( $module, $params ); } @@ -1307,7 +1442,7 @@ class ApiMain extends ApiBase { ); } - public function modifyHelp( array &$help, array $options ) { + public function modifyHelp( array &$help, array $options, array &$tocData ) { // Wish PHP had an "array_insert_before". Instead, we have to manually // reindex the array to get 'permissions' in the right place. $oldHelp = $help; @@ -1318,6 +1453,7 @@ class ApiMain extends ApiBase { } $help[$k] = $v; } + $help['datatypes'] = ''; $help['credits'] = ''; // Fill 'permissions' @@ -1350,13 +1486,48 @@ class ApiMain extends ApiBase { $help['permissions'] .= Html::closeElement( 'dl' ); $help['permissions'] .= Html::closeElement( 'div' ); - // Fill 'credits', if applicable + // Fill 'datatypes' and 'credits', if applicable if ( empty( $options['nolead'] ) ) { - $help['credits'] .= Html::element( 'h' . min( 6, $options['headerlevel'] + 1 ), - array( 'id' => '+credits', 'class' => 'apihelp-header' ), - $this->msg( 'api-credits-header' )->parse() + $level = $options['headerlevel']; + $tocnumber = &$options['tocnumber']; + + $header = $this->msg( 'api-help-datatypes-header' )->parse(); + $help['datatypes'] .= Html::rawelement( 'h' . min( 6, $level ), + array( 'id' => 'main/datatypes', 'class' => 'apihelp-header' ), + Html::element( 'span', array( 'id' => Sanitizer::escapeId( 'main/datatypes' ) ) ) . + $header + ); + $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock(); + if ( !isset( $tocData['main/datatypes'] ) ) { + $tocnumber[$level]++; + $tocData['main/datatypes'] = array( + 'toclevel' => count( $tocnumber ), + 'level' => $level, + 'anchor' => 'main/datatypes', + 'line' => $header, + 'number' => join( '.', $tocnumber ), + 'index' => false, + ); + } + + $header = $this->msg( 'api-credits-header' )->parse(); + $help['credits'] .= Html::rawelement( 'h' . min( 6, $level ), + array( 'id' => 'main/credits', 'class' => 'apihelp-header' ), + Html::element( 'span', array( 'id' => Sanitizer::escapeId( 'main/credits' ) ) ) . + $header ); $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock(); + if ( !isset( $tocData['main/credits'] ) ) { + $tocnumber[$level]++; + $tocData['main/credits'] = array( + 'toclevel' => count( $tocnumber ), + 'level' => $level, + 'anchor' => 'main/credits', + 'line' => $header, + 'number' => join( '.', $tocnumber ), + 'index' => false, + ); + } } } |