* have been accumulated, and replace it with an error message and a help screen.
*/
protected function executeActionWithErrorHandling() {
+ // Verify the CORS header before executing the action
+ if ( !$this->handleCORS() ) {
+ // handleCORS() has sent a 403, abort
+ return;
+ }
+
// In case an error occurs during data output,
// clear the output buffer and print just the error information
ob_start();
ob_end_flush();
}
+ /**
+ * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
+ *
+ * If no origin parameter is present, nothing happens.
+ * If an origin parameter is present but doesn't match the Origin header, a 403 status code
+ * is set and false is returned.
+ * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
+ * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
+ * headers are set.
+ *
+ * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
+ */
+ protected function handleCORS() {
+ global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions;
+ $response = $this->getRequest()->response();
+ $originParam = $this->getParameter( 'origin' ); // defaults to null
+ if ( $originParam === null ) {
+ // No origin parameter, nothing to do
+ return true;
+ }
+ // Origin: header is a space-separated list of origins, check all of them
+ $originHeader = isset( $_SERVER['HTTP_ORIGIN'] ) ? $_SERVER['HTTP_ORIGIN'] : '';
+ $origins = explode( ' ', $originHeader );
+ 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->header( 'Cache-Control: no-cache' );
+ echo "'origin' parameter does not match Origin header\n";
+ return false;
+ }
+ if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) {
+ $response->header( "Access-Control-Allow-Origin: $originParam" );
+ $response->header( 'Access-Control-Allow-Credentials: true' );
+ $this->getOutput()->addVaryHeader( 'Origin' );
+ }
+ return true;
+ }
+
+ /**
+ * Attempt to match an Origin header against a set of rules and a set of exceptions
+ * @param $value string Origin header
+ * @param $rules array Set of wildcard rules
+ * @param $exceptions array Set of wildcard rules
+ * @return bool True if $value matches a rule in $rules and doesn't match any rules in $exceptions, false otherwise
+ */
+ protected static function matchOrigin( $value, $rules, $exceptions ) {
+ foreach ( $rules as $rule ) {
+ if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
+ // Rule matches, check exceptions
+ foreach ( $exceptions as $exc ) {
+ if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Helper function to convert wildcard string into a regex
+ * '*' => '.*?'
+ * '?' => '.'
+ *
+ * @param $wildcard string String with wildcards
+ * @return string Regular expression
+ */
+ protected static function wildcardToRegex( $wildcard ) {
+ $wildcard = preg_quote( $wildcard, '/' );
+ $wildcard = str_replace(
+ array( '\*', '\?' ),
+ array( '.*?', '.' ),
+ $wildcard
+ );
+ return "/https?:\/\/$wildcard/";
+ }
+
protected function sendCacheHeaders() {
global $wgUseXVO, $wgVaryOnXFP;
$response = $this->getRequest()->response();
+ $out = $this->getOutput();
+
+ if ( $wgVaryOnXFP ) {
+ $out->addVaryHeader( 'X-Forwarded-Proto' );
+ }
if ( $this->mCacheMode == 'private' ) {
$response->header( 'Cache-Control: private' );
}
if ( $this->mCacheMode == 'anon-public-user-private' ) {
- $xfp = $wgVaryOnXFP ? ', X-Forwarded-Proto' : '';
- $response->header( 'Vary: Accept-Encoding, Cookie' . $xfp );
+ $out->addVaryHeader( 'Cookie' );
+ $response->header( $out->getVaryHeader() );
if ( $wgUseXVO ) {
- $out = $this->getOutput();
- if ( $wgVaryOnXFP ) {
- $out->addVaryHeader( 'X-Forwarded-Proto' );
- }
$response->header( $out->getXVO() );
if ( $out->haveCacheVaryCookies() ) {
// Logged in, mark this request private
}
// Send public headers
- if ( $wgVaryOnXFP ) {
- $response->header( 'Vary: Accept-Encoding, X-Forwarded-Proto' );
- if ( $wgUseXVO ) {
- // Bleeeeegh. Our header setting system sucks
- $response->header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip, X-Forwarded-Proto' );
- }
+ $response->header( $out->getVaryHeader() );
+ if ( $wgUseXVO ) {
+ $response->header( $out->getXVO() );
}
// If nobody called setCacheMaxAge(), use the (s)maxage parameters
),
'requestid' => null,
'servedby' => false,
+ 'origin' => null,
);
}
'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error',
+ 'origin' => array(
+ 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.',
+ '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 .',
+ 'If this parameter does not match the Origin: header, a 403 response will be returned.',
+ 'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.',
+ ),
);
}