X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fapi%2FApiMain.php;h=c558c2b455a4a389a20f2671b32c562a7be37057;hb=d92845c2295d982c80312880c3fae788450676ea;hp=8ce505a0d75f1bd3377879e791a183f28a4ee915;hpb=9160785c6fb478a929502372ce00a0560f0de936;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 8ce505a0d7..c558c2b455 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -425,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 @@ -439,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(); + } } /** @@ -532,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(); } @@ -701,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(); @@ -711,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. @@ -993,6 +1018,121 @@ class ApiMain extends ApiBase { return true; } + /** + * 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 @@ -1083,20 +1223,18 @@ class ApiMain extends ApiBase { return; } + if ( !$this->checkConditionalRequestHeaders( $module ) ) { + return; + } + if ( !$this->mInternalMode ) { $this->setupExternalResponse( $module, $params ); } $this->checkAsserts( $params ); - $stats = $this->getContext()->getStats(); - $statsPath = 'api.modules.' . strtr( $module->getModulePath(), '+', '.' ); - $metric = $stats->increment( $statsPath ); - $metric->setSampleRate( 0.001 ); - // Execute $module->execute(); - Hooks::run( 'APIAfterExecute', array( &$module ) ); $this->reportUnusedParams();