2020-08-22 19:26:19 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MediaWiki\Tests\Rest;
|
|
|
|
|
|
|
|
|
|
use MediaWiki\Config\ServiceOptions;
|
2022-08-17 20:33:06 +00:00
|
|
|
use MediaWiki\MainConfigNames;
|
2020-08-22 19:26:19 +00:00
|
|
|
use MediaWiki\Rest\CorsUtils;
|
|
|
|
|
use MediaWiki\Rest\Handler;
|
|
|
|
|
use MediaWiki\Rest\RequestInterface;
|
|
|
|
|
use MediaWiki\Rest\Response;
|
|
|
|
|
use MediaWiki\Rest\ResponseFactory;
|
|
|
|
|
use MediaWiki\Rest\ResponseInterface;
|
2021-04-21 10:16:42 +00:00
|
|
|
use MediaWiki\User\UserIdentityValue;
|
2020-08-22 19:26:19 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers \MediaWiki\Rest\CorsUtils
|
|
|
|
|
*/
|
|
|
|
|
class CorsUtilsTest extends \MediaWikiUnitTestCase {
|
|
|
|
|
|
|
|
|
|
private function createServiceOptions( array $options = [] ) {
|
|
|
|
|
$defaults = [
|
2022-08-17 20:33:06 +00:00
|
|
|
MainConfigNames::AllowedCorsHeaders => [],
|
|
|
|
|
MainConfigNames::AllowCrossOrigin => false,
|
|
|
|
|
MainConfigNames::RestAllowCrossOriginCookieAuth => false,
|
|
|
|
|
MainConfigNames::CanonicalServer => 'https://example.com',
|
|
|
|
|
MainConfigNames::CrossSiteAJAXdomains => [],
|
|
|
|
|
MainConfigNames::CrossSiteAJAXdomainExceptions => [],
|
2020-08-22 19:26:19 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return new ServiceOptions( CorsUtils::CONSTRUCTOR_OPTIONS, array_merge( $defaults, $options ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideAuthorizeAllowOrigin
|
|
|
|
|
*/
|
|
|
|
|
public function testAuthorizeAllowOrigin( bool $isRegistered, bool $needsWriteAccess, string $origin ) {
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions( [
|
2022-08-17 20:33:06 +00:00
|
|
|
MainConfigNames::CrossSiteAJAXdomains => [
|
2020-08-22 19:26:19 +00:00
|
|
|
'www.mediawiki.org',
|
|
|
|
|
],
|
|
|
|
|
] ),
|
|
|
|
|
$this->createNoOpMock( ResponseFactory::class ),
|
2022-08-05 10:37:28 +00:00
|
|
|
new UserIdentityValue( (int)$isRegistered, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$request = $this->createMock( RequestInterface::class );
|
|
|
|
|
$request->method( 'hasHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', (bool)$origin ]
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
$request->method( 'getHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', [ $origin ] ]
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
|
|
|
|
|
$handler = $this->createMock( Handler::class );
|
|
|
|
|
$handler->method( 'needsWriteAccess' )
|
|
|
|
|
->willReturn( false );
|
|
|
|
|
|
|
|
|
|
$result = $cors->authorize(
|
|
|
|
|
$request,
|
|
|
|
|
$handler
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertNull( $result );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideAuthorizeAllowOrigin() {
|
|
|
|
|
$origin = 'https://example.com';
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'User is registered' => [
|
|
|
|
|
true,
|
|
|
|
|
true,
|
|
|
|
|
$origin,
|
|
|
|
|
],
|
|
|
|
|
'Handler does not need write access' => [
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
$origin,
|
|
|
|
|
],
|
|
|
|
|
'Missing origin' => [
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
'',
|
|
|
|
|
],
|
|
|
|
|
'Same origin' => [
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
$origin,
|
|
|
|
|
],
|
|
|
|
|
'Trusted origin' => [
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
'https://www.mediawiki.org',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAuthorizeDisallowOrigin() {
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions(),
|
|
|
|
|
$this->createMock( ResponseFactory::class ),
|
2022-08-05 10:37:28 +00:00
|
|
|
new UserIdentityValue( 0, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$request = $this->createMock( RequestInterface::class );
|
|
|
|
|
$request->method( 'hasHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', true ]
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
$request->expects( $this->once() )
|
|
|
|
|
->method( 'getHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', [ 'https://www.mediawiki.org' ] ]
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
|
|
|
|
|
$handler = $this->createMock( Handler::class );
|
|
|
|
|
$handler->method( 'needsWriteAccess' )
|
|
|
|
|
->willReturn( true );
|
|
|
|
|
|
|
|
|
|
$result = $cors->authorize(
|
|
|
|
|
$request,
|
|
|
|
|
$handler
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'rest-cross-origin-anon-write', $result );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testModifyResponseNoChange() {
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions(),
|
|
|
|
|
$this->createMock( ResponseFactory::class ),
|
2021-04-21 10:16:42 +00:00
|
|
|
new UserIdentityValue( 0, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$response = $this->createNoOpMock( ResponseInterface::class );
|
|
|
|
|
|
2022-08-05 10:37:28 +00:00
|
|
|
$result = $cors->modifyResponse(
|
|
|
|
|
$this->createMock( RequestInterface::class ),
|
|
|
|
|
$response
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $response, $result );
|
2020-08-22 19:26:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testModifyResponseAllowOrigin() {
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions( [
|
2022-08-17 20:33:06 +00:00
|
|
|
MainConfigNames::AllowCrossOrigin => true,
|
2020-08-22 19:26:19 +00:00
|
|
|
] ),
|
|
|
|
|
$this->createNoOpMock( ResponseFactory::class ),
|
2021-04-21 10:16:42 +00:00
|
|
|
new UserIdentityValue( 0, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$response = new Response();
|
|
|
|
|
|
|
|
|
|
$result = $cors->modifyResponse(
|
|
|
|
|
$this->createMock( RequestInterface::class ),
|
|
|
|
|
$response
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $response, $result );
|
|
|
|
|
$this->assertFalse( $result->hasHeader( 'Vary' ) );
|
|
|
|
|
$this->assertTrue( $result->hasHeader( 'Access-Control-Allow-Origin' ) );
|
|
|
|
|
$this->assertSame( [ '*' ], $result->getHeader( 'Access-Control-Allow-Origin' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideModifyResponseAllowTrustedOriginCookieAuth
|
|
|
|
|
* @param string $requestMethod
|
|
|
|
|
* @param string $isRegistered
|
|
|
|
|
*/
|
|
|
|
|
public function testModifyResponseAllowTrustedOriginCookieAuth( string $requestMethod, bool $isRegistered ) {
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions( [
|
2022-08-17 20:33:06 +00:00
|
|
|
MainConfigNames::AllowCrossOrigin => true,
|
|
|
|
|
MainConfigNames::RestAllowCrossOriginCookieAuth => true,
|
2020-08-22 19:26:19 +00:00
|
|
|
] ),
|
|
|
|
|
$this->createNoOpMock( ResponseFactory::class ),
|
2022-08-05 10:37:28 +00:00
|
|
|
new UserIdentityValue( (int)$isRegistered, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$request = $this->createMock( RequestInterface::class );
|
|
|
|
|
$request->method( 'hasHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', true ]
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
$request->method( 'getHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', [ 'https://example.com' ] ],
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
$request->method( 'getMethod' )
|
|
|
|
|
->willReturn( $requestMethod );
|
|
|
|
|
|
|
|
|
|
$response = new Response();
|
|
|
|
|
|
|
|
|
|
$result = $cors->modifyResponse( $request, $response );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $response, $result );
|
|
|
|
|
$this->assertTrue( $result->hasHeader( 'Vary' ) );
|
|
|
|
|
$this->assertSame( [ 'Origin' ], $result->getHeader( 'Vary' ) );
|
|
|
|
|
$this->assertTrue( $result->hasHeader( 'Access-Control-Allow-Credentials' ) );
|
|
|
|
|
$this->assertSame( [ 'true' ], $result->getHeader( 'Access-Control-Allow-Credentials' ) );
|
|
|
|
|
$this->assertTrue( $result->hasHeader( 'Access-Control-Allow-Origin' ) );
|
|
|
|
|
$this->assertSame( [ 'https://example.com' ], $result->getHeader( 'Access-Control-Allow-Origin' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideModifyResponseAllowTrustedOriginCookieAuth() {
|
|
|
|
|
return [
|
|
|
|
|
'OPTIONS request' => [
|
|
|
|
|
'OPTIONS',
|
|
|
|
|
false
|
|
|
|
|
],
|
|
|
|
|
'Registered user on main request' => [
|
|
|
|
|
'POST',
|
|
|
|
|
true,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideModifyResponseDisallowUntrustedOriginCookieAuth
|
|
|
|
|
*/
|
|
|
|
|
public function testModifyResponseDisallowUntrustedOriginCookieAuth(
|
|
|
|
|
string $origin,
|
|
|
|
|
string $requestMethod,
|
|
|
|
|
bool $isRegistered
|
|
|
|
|
) {
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions( [
|
2022-08-17 20:33:06 +00:00
|
|
|
MainConfigNames::AllowCrossOrigin => true,
|
|
|
|
|
MainConfigNames::RestAllowCrossOriginCookieAuth => true,
|
2020-08-22 19:26:19 +00:00
|
|
|
] ),
|
|
|
|
|
$this->createNoOpMock( ResponseFactory::class ),
|
2022-08-05 10:37:28 +00:00
|
|
|
new UserIdentityValue( (int)$isRegistered, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$request = $this->createMock( RequestInterface::class );
|
|
|
|
|
$request->method( 'hasHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', (bool)$origin ]
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
$request->method( 'getHeader' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnMap( [
|
2020-08-22 19:26:19 +00:00
|
|
|
[ 'Origin', [ $origin ] ],
|
2022-06-05 23:39:02 +00:00
|
|
|
] );
|
2020-08-22 19:26:19 +00:00
|
|
|
$request->method( 'getMethod' )
|
|
|
|
|
->willReturn( $requestMethod );
|
|
|
|
|
|
|
|
|
|
$response = new Response();
|
|
|
|
|
|
|
|
|
|
$result = $cors->modifyResponse( $request, $response );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $response, $result );
|
|
|
|
|
$this->assertTrue( $result->hasHeader( 'Vary' ) );
|
|
|
|
|
$this->assertSame( [ 'Origin' ], $result->getHeader( 'Vary' ) );
|
|
|
|
|
$this->assertFalse( $result->hasHeader( 'Access-Control-Allow-Credentials' ) );
|
|
|
|
|
$this->assertTrue( $result->hasHeader( 'Access-Control-Allow-Origin' ) );
|
|
|
|
|
$this->assertSame( [ '*' ], $result->getHeader( 'Access-Control-Allow-Origin' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideModifyResponseDisallowUntrustedOriginCookieAuth() {
|
|
|
|
|
return [
|
|
|
|
|
'Missing Origin' => [
|
|
|
|
|
'',
|
|
|
|
|
'GET',
|
|
|
|
|
true,
|
|
|
|
|
],
|
|
|
|
|
'Untrusted Origin' => [
|
|
|
|
|
'www.mediawiki.org',
|
|
|
|
|
'GET',
|
|
|
|
|
true
|
|
|
|
|
],
|
|
|
|
|
'Trusted Origin, anon user' => [
|
|
|
|
|
'example.com',
|
|
|
|
|
'POST',
|
|
|
|
|
false
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreatePreflightResponse() {
|
|
|
|
|
$responseFactory = $this->createMock( ResponseFactory::class );
|
|
|
|
|
$responseFactory->method( 'createNoContent' )
|
|
|
|
|
->willReturn( new Response() );
|
|
|
|
|
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions(),
|
|
|
|
|
$responseFactory,
|
2021-04-21 10:16:42 +00:00
|
|
|
new UserIdentityValue( 0, __CLASS__ )
|
2020-08-22 19:26:19 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$methods = [ 'POST' ];
|
|
|
|
|
$response = $cors->createPreflightResponse( $methods );
|
|
|
|
|
|
|
|
|
|
$this->assertInstanceOf( ResponseInterface::class, $response );
|
|
|
|
|
$this->assertTrue( $response->hasHeader( 'Access-Control-Allow-Headers' ) );
|
|
|
|
|
$this->assertTrue( $response->hasHeader( 'Access-Control-Allow-Methods' ) );
|
|
|
|
|
$this->assertSame( $methods, $response->getHeader( 'Access-Control-Allow-Methods' ) );
|
|
|
|
|
}
|
2021-05-22 00:32:58 +00:00
|
|
|
|
|
|
|
|
public function testCreatePreflightResponse_allow_headers() {
|
|
|
|
|
$responseFactory = $this->createMock( ResponseFactory::class );
|
|
|
|
|
$responseFactory->method( 'createNoContent' )
|
|
|
|
|
->willReturn( new Response() );
|
|
|
|
|
|
|
|
|
|
$cors = new CorsUtils(
|
|
|
|
|
$this->createServiceOptions( [
|
2022-08-17 20:33:06 +00:00
|
|
|
MainConfigNames::AllowedCorsHeaders => [ 'Authorization', 'BlaHeader', ],
|
2021-05-22 00:32:58 +00:00
|
|
|
] ),
|
|
|
|
|
$responseFactory,
|
|
|
|
|
new UserIdentityValue( 0, __CLASS__ )
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$methods = [ 'POST' ];
|
|
|
|
|
$response = $cors->createPreflightResponse( $methods );
|
|
|
|
|
$this->assertTrue( $response->hasHeader( 'Access-Control-Allow-Headers' ) );
|
|
|
|
|
$header = $response->getHeader( 'Access-Control-Allow-Headers' );
|
|
|
|
|
$this->assertContains( 'Authorization', $header );
|
|
|
|
|
$this->assertContains( 'Content-Type', $header );
|
|
|
|
|
$this->assertSame( count( $header ), count( array_unique( $header ) ) );
|
|
|
|
|
}
|
2020-08-22 19:26:19 +00:00
|
|
|
}
|