2013-10-23 15:36:40 +00:00
|
|
|
<?php
|
|
|
|
|
|
2018-11-26 18:31:41 +00:00
|
|
|
use Wikimedia\Rdbms\DBQueryError;
|
2017-04-19 19:37:35 +00:00
|
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
|
|
2013-10-23 15:36:40 +00:00
|
|
|
/**
|
|
|
|
|
* @group API
|
2017-03-18 23:06:09 +00:00
|
|
|
* @group Database
|
2013-10-23 15:36:40 +00:00
|
|
|
* @group medium
|
|
|
|
|
*
|
|
|
|
|
* @covers ApiMain
|
|
|
|
|
*/
|
|
|
|
|
class ApiMainTest extends ApiTestCase {
|
|
|
|
|
|
|
|
|
|
/**
|
API: HTMLize and internationalize the help, add Special:ApiHelp
The existing API help, formatted as basically a plain-text document
embedded in XML and with a little bolding and a few links
syntax-highlighted in after the fact, works ok for experienced programmers
but isn't at all newbie-friendly. Further, all the help is hard-coded in
English, which isn't very friendly to non-English speakers.
So let's rewrite it. The help text is now obtained from i18n messages
and output in HTML, with the default display consisting of help for a
single module with links to help for other modules. This, of course,
necessitates deprecating many of the existing help-related methods and
hooks and replacing them with new ones, but backwards compatibility is
maintained for almost everything.
At the same time, action=paraminfo also needs to support the
'description' and other help-related fields being output in wikitext or
HTML, and I11cb063d (to access all modules via the 'modules' parameter
instead of having 'modules', 'formatmodules', 'querymodules', and so on)
is folded in.
And we also add Special:ApiHelp. When directly accessed, it simply
redirects to api.php with appropriate parameters. But it's also
transcludable to allow up-to-date API help text to be included within
the on-wiki documentation.
Note this patch doesn't actually add i18n messages for any API modules
besides ApiMain and ApiHelp. That will come in a followup patch, but for
the moment the backwards-compatibility code handles them nicely.
While we're messing with the documentation, we may as well add the
"internal" flag requested in bug 62905 (although the 'includeinternal'
parameter it also requests doesn't make much sense anymore) and a
"deprecated" flag that's needed by several modules now.
Bug: 30936
Bug: 38126
Bug: 42343
Bug: 45641
Bug: 62905
Bug: 63211
Change-Id: Ib14c00df06d85c2f6364d83b2b10ce34c7f513cc
2014-09-16 17:54:01 +00:00
|
|
|
* Test that the API will accept a FauxRequest and execute.
|
2013-10-23 15:36:40 +00:00
|
|
|
*/
|
|
|
|
|
public function testApi() {
|
|
|
|
|
$api = new ApiMain(
|
2016-02-17 09:09:32 +00:00
|
|
|
new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] )
|
2013-10-23 15:36:40 +00:00
|
|
|
);
|
|
|
|
|
$api->execute();
|
API: Overhaul ApiResult, make format=xml not throw, and add json formatversion
ApiResult was a mess: some methods could only be used with an array
reference instead of manipulating the stored data, methods that had both
array-ref and internal-data versions had names that didn't at all
correspond, some methods that worked on an array reference were
annoyingly non-static, and then the whole mess with setIndexedTagName.
ApiFormatXml is also entirely annoying to deal with, as it liked to
throw exceptions if certain metadata wasn't provided that no other
formatter required. Its legacy also means we have this silly convention
of using empty-string rather than boolean true, annoying restrictions on
keys (leading to things that should be hashes being arrays of key-value
object instead), '*' used as a key all over the place, and so on.
So, changes here:
* ApiResult is no longer an ApiBase or a ContextSource.
* Wherever sensible, ApiResult provides a static method working on an
arrayref and a non-static method working on internal data.
* Metadata is now always added to ApiResult's internal data structure.
Formatters are responsible for stripping it if necessary. "raw mode"
is deprecated.
* New metadata to replace the '*' key, solve the array() => '[]' vs '{}'
question, and so on.
* New class for formatting warnings and errors using i18n messages, and
support for multiple errors and a more machine-readable format for
warnings. For the moment, though, the actual output will not be changing
yet (see T47843 for future plans).
* New formatversion parameter for format=json and format=php, to select
between BC mode and the modern output.
* In BC mode, booleans will be converted to empty-string presence style;
modules currently returning booleans will need to use
ApiResult::META_BC_BOOLS to preserve their current output.
Actual changes to the API modules' output (e.g. actually returning
booleans for the new formatversion) beyond the use of
ApiResult::setContentValue() are left for a future change.
Bug: T76728
Bug: T57371
Bug: T33629
Change-Id: I7b37295e8862b188d1f3b0cd07f66ac34629678f
2014-12-03 22:14:22 +00:00
|
|
|
$data = $api->getResult()->getResultData();
|
API: HTMLize and internationalize the help, add Special:ApiHelp
The existing API help, formatted as basically a plain-text document
embedded in XML and with a little bolding and a few links
syntax-highlighted in after the fact, works ok for experienced programmers
but isn't at all newbie-friendly. Further, all the help is hard-coded in
English, which isn't very friendly to non-English speakers.
So let's rewrite it. The help text is now obtained from i18n messages
and output in HTML, with the default display consisting of help for a
single module with links to help for other modules. This, of course,
necessitates deprecating many of the existing help-related methods and
hooks and replacing them with new ones, but backwards compatibility is
maintained for almost everything.
At the same time, action=paraminfo also needs to support the
'description' and other help-related fields being output in wikitext or
HTML, and I11cb063d (to access all modules via the 'modules' parameter
instead of having 'modules', 'formatmodules', 'querymodules', and so on)
is folded in.
And we also add Special:ApiHelp. When directly accessed, it simply
redirects to api.php with appropriate parameters. But it's also
transcludable to allow up-to-date API help text to be included within
the on-wiki documentation.
Note this patch doesn't actually add i18n messages for any API modules
besides ApiMain and ApiHelp. That will come in a followup patch, but for
the moment the backwards-compatibility code handles them nicely.
While we're messing with the documentation, we may as well add the
"internal" flag requested in bug 62905 (although the 'includeinternal'
parameter it also requests doesn't make much sense anymore) and a
"deprecated" flag that's needed by several modules now.
Bug: 30936
Bug: 38126
Bug: 42343
Bug: 45641
Bug: 62905
Bug: 63211
Change-Id: Ib14c00df06d85c2f6364d83b2b10ce34c7f513cc
2014-09-16 17:54:01 +00:00
|
|
|
$this->assertInternalType( 'array', $data );
|
|
|
|
|
$this->assertArrayHasKey( 'query', $data );
|
2013-10-23 15:36:40 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-26 14:38:41 +00:00
|
|
|
public function testApiNoParam() {
|
|
|
|
|
$api = new ApiMain();
|
|
|
|
|
$api->execute();
|
|
|
|
|
$data = $api->getResult()->getResultData();
|
|
|
|
|
$this->assertInternalType( 'array', $data );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ApiMain behaves differently if passed a FauxRequest (mInternalMode set
|
|
|
|
|
* to true) or a proper WebRequest (mInternalMode false). For most tests
|
|
|
|
|
* we can just set mInternalMode to false using TestingAccessWrapper, but
|
|
|
|
|
* this doesn't work for the constructor. This method returns an ApiMain
|
|
|
|
|
* that's been set up in non-internal mode.
|
|
|
|
|
*
|
|
|
|
|
* Note that calling execute() will print to the console. Wrap it in
|
|
|
|
|
* ob_start()/ob_end_clean() to prevent this.
|
|
|
|
|
*
|
|
|
|
|
* @param array $requestData Query parameters for the WebRequest
|
|
|
|
|
* @param array $headers Headers for the WebRequest
|
|
|
|
|
*/
|
|
|
|
|
private function getNonInternalApiMain( array $requestData, array $headers = [] ) {
|
|
|
|
|
$req = $this->getMockBuilder( WebRequest::class )
|
2018-04-12 17:00:35 +00:00
|
|
|
->setMethods( [ 'response', 'getRawIP' ] )
|
2018-03-26 14:38:41 +00:00
|
|
|
->getMock();
|
|
|
|
|
$response = new FauxResponse();
|
|
|
|
|
$req->method( 'response' )->willReturn( $response );
|
|
|
|
|
$req->method( 'getRawIP' )->willReturn( '127.0.0.1' );
|
|
|
|
|
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $req );
|
|
|
|
|
$wrapper->data = $requestData;
|
|
|
|
|
if ( $headers ) {
|
|
|
|
|
$wrapper->headers = $headers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new ApiMain( $req );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testUselang() {
|
|
|
|
|
global $wgLang;
|
|
|
|
|
|
|
|
|
|
$api = $this->getNonInternalApiMain( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
'uselang' => 'fr',
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
|
$api->execute();
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'fr', $wgLang->getCode() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testNonWhitelistedCorsWithCookies() {
|
|
|
|
|
$logFile = $this->getNewTempFile();
|
|
|
|
|
|
|
|
|
|
$this->mergeMwGlobalArrayValue( '_COOKIE', [ 'forceHTTPS' => '1' ] );
|
|
|
|
|
$logger = new TestLogger( true );
|
|
|
|
|
$this->setLogger( 'cors', $logger );
|
|
|
|
|
|
|
|
|
|
$api = $this->getNonInternalApiMain( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
// For some reason multiple origins (which are not allowed in the
|
|
|
|
|
// WHATWG Fetch spec that supersedes the RFC) are always considered to
|
|
|
|
|
// be problematic.
|
|
|
|
|
], [ 'ORIGIN' => 'https://www.example.com https://www.com.example' ] );
|
|
|
|
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
[ [ Psr\Log\LogLevel::WARNING, 'Non-whitelisted CORS request with session cookies' ] ],
|
|
|
|
|
$logger->getBuffer()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSuppressedLogin() {
|
|
|
|
|
global $wgUser;
|
|
|
|
|
$origUser = $wgUser;
|
|
|
|
|
|
|
|
|
|
$api = $this->getNonInternalApiMain( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
'origin' => '*',
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
|
$api->execute();
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
|
|
|
|
|
$this->assertNotSame( $origUser, $wgUser );
|
|
|
|
|
$this->assertSame( 'true', $api->getContext()->getRequest()->response()
|
|
|
|
|
->getHeader( 'MediaWiki-Login-Suppressed' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetContinuationManager() {
|
|
|
|
|
$api = new ApiMain();
|
|
|
|
|
$manager = $this->createMock( ApiContinuationManager::class );
|
|
|
|
|
$api->setContinuationManager( $manager );
|
|
|
|
|
$this->assertTrue( true, 'No exception' );
|
|
|
|
|
return [ $api, $manager ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @depends testSetContinuationManager
|
|
|
|
|
*/
|
|
|
|
|
public function testSetContinuationManagerTwice( $args ) {
|
|
|
|
|
$this->setExpectedException( UnexpectedValueException::class,
|
|
|
|
|
'ApiMain::setContinuationManager: tried to set manager from ' .
|
|
|
|
|
'when a manager is already set from ' );
|
|
|
|
|
|
|
|
|
|
list( $api, $manager ) = $args;
|
|
|
|
|
$api->setContinuationManager( $manager );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetCacheModeUnrecognized() {
|
|
|
|
|
$api = new ApiMain();
|
|
|
|
|
$api->setCacheMode( 'unrecognized' );
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
'private',
|
|
|
|
|
TestingAccessWrapper::newFromObject( $api )->mCacheMode,
|
|
|
|
|
'Unrecognized params must be silently ignored'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetCacheModePrivateWiki() {
|
|
|
|
|
$this->setGroupPermissions( '*', 'read', false );
|
|
|
|
|
$wrappedApi = TestingAccessWrapper::newFromObject( new ApiMain() );
|
|
|
|
|
$wrappedApi->setCacheMode( 'public' );
|
|
|
|
|
$this->assertSame( 'private', $wrappedApi->mCacheMode );
|
|
|
|
|
$wrappedApi->setCacheMode( 'anon-public-user-private' );
|
|
|
|
|
$this->assertSame( 'private', $wrappedApi->mCacheMode );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddRequestedFieldsRequestId() {
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
'requestid' => '123456',
|
|
|
|
|
] );
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->execute();
|
|
|
|
|
$this->assertSame( '123456', $api->getResult()->getResultData()['requestid'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddRequestedFieldsCurTimestamp() {
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
'curtimestamp' => '',
|
|
|
|
|
] );
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->execute();
|
|
|
|
|
$timestamp = $api->getResult()->getResultData()['curtimestamp'];
|
|
|
|
|
$this->assertLessThanOrEqual( 1, abs( strtotime( $timestamp ) - time() ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddRequestedFieldsResponseLangInfo() {
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
// errorlang is ignored if errorformat is not specified
|
|
|
|
|
'errorformat' => 'plaintext',
|
|
|
|
|
'uselang' => 'FR',
|
|
|
|
|
'errorlang' => 'ja',
|
|
|
|
|
'responselanginfo' => '',
|
|
|
|
|
] );
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->execute();
|
|
|
|
|
$data = $api->getResult()->getResultData();
|
|
|
|
|
$this->assertSame( 'fr', $data['uselang'] );
|
|
|
|
|
$this->assertSame( 'ja', $data['errorlang'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetupModuleUnknown() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'Unrecognized value for parameter "action": unknownaction.' );
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest( [ 'action' => 'unknownaction' ] );
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetupModuleNoTokenProvided() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'The "token" parameter must be set.' );
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'edit',
|
|
|
|
|
'title' => 'New page',
|
|
|
|
|
'text' => 'Some text',
|
|
|
|
|
] );
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetupModuleInvalidTokenProvided() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class, 'Invalid CSRF token.' );
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'edit',
|
|
|
|
|
'title' => 'New page',
|
|
|
|
|
'text' => 'Some text',
|
|
|
|
|
'token' => "This isn't a real token!",
|
|
|
|
|
] );
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetupModuleNeedsTokenTrue() {
|
|
|
|
|
$this->setExpectedException( MWException::class,
|
|
|
|
|
"Module 'testmodule' must be updated for the new token handling. " .
|
|
|
|
|
"See documentation for ApiBase::needsToken for details." );
|
|
|
|
|
|
|
|
|
|
$mock = $this->createMock( ApiBase::class );
|
|
|
|
|
$mock->method( 'getModuleName' )->willReturn( 'testmodule' );
|
|
|
|
|
$mock->method( 'needsToken' )->willReturn( true );
|
|
|
|
|
|
|
|
|
|
$api = new ApiMain( new FauxRequest( [ 'action' => 'testmodule' ] ) );
|
|
|
|
|
$api->getModuleManager()->addModule( 'testmodule', 'action', get_class( $mock ),
|
|
|
|
|
function () use ( $mock ) {
|
|
|
|
|
return $mock;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$api->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetupModuleNeedsTokenNeedntBePosted() {
|
|
|
|
|
$this->setExpectedException( MWException::class,
|
|
|
|
|
"Module 'testmodule' must require POST to use tokens." );
|
|
|
|
|
|
|
|
|
|
$mock = $this->createMock( ApiBase::class );
|
|
|
|
|
$mock->method( 'getModuleName' )->willReturn( 'testmodule' );
|
|
|
|
|
$mock->method( 'needsToken' )->willReturn( 'csrf' );
|
|
|
|
|
$mock->method( 'mustBePosted' )->willReturn( false );
|
|
|
|
|
|
|
|
|
|
$api = new ApiMain( new FauxRequest( [ 'action' => 'testmodule' ] ) );
|
|
|
|
|
$api->getModuleManager()->addModule( 'testmodule', 'action', get_class( $mock ),
|
|
|
|
|
function () use ( $mock ) {
|
|
|
|
|
return $mock;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$api->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckMaxLagFailed() {
|
|
|
|
|
// It's hard to mock the LoadBalancer properly, so instead we'll mock
|
|
|
|
|
// checkMaxLag (which is tested directly in other tests below).
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$mock = $this->getMockBuilder( ApiMain::class )
|
|
|
|
|
->setConstructorArgs( [ $req ] )
|
|
|
|
|
->setMethods( [ 'checkMaxLag' ] )
|
|
|
|
|
->getMock();
|
|
|
|
|
$mock->method( 'checkMaxLag' )->willReturn( false );
|
|
|
|
|
|
|
|
|
|
$mock->execute();
|
|
|
|
|
|
|
|
|
|
$this->assertArrayNotHasKey( 'query', $mock->getResult()->getResultData() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckConditionalRequestHeadersFailed() {
|
|
|
|
|
// The detailed checking of all cases of checkConditionalRequestHeaders
|
|
|
|
|
// is below in testCheckConditionalRequestHeaders(), which calls the
|
|
|
|
|
// method directly. Here we just check that it will stop execution if
|
|
|
|
|
// it does fail.
|
|
|
|
|
$now = time();
|
|
|
|
|
|
|
|
|
|
$this->setMwGlobals( 'wgCacheEpoch', '20030516000000' );
|
|
|
|
|
|
|
|
|
|
$mock = $this->createMock( ApiBase::class );
|
|
|
|
|
$mock->method( 'getModuleName' )->willReturn( 'testmodule' );
|
|
|
|
|
$mock->method( 'getConditionalRequestData' )
|
|
|
|
|
->willReturn( wfTimestamp( TS_MW, $now - 3600 ) );
|
|
|
|
|
$mock->expects( $this->exactly( 0 ) )->method( 'execute' );
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'testmodule',
|
|
|
|
|
] );
|
|
|
|
|
$req->setHeader( 'If-Modified-Since', wfTimestamp( TS_RFC2822, $now - 3600 ) );
|
|
|
|
|
$req->setRequestURL( "http://localhost" );
|
|
|
|
|
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$api->getModuleManager()->addModule( 'testmodule', 'action', get_class( $mock ),
|
|
|
|
|
function () use ( $mock ) {
|
|
|
|
|
return $mock;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $api );
|
|
|
|
|
$wrapper->mInternalMode = false;
|
|
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
|
$api->execute();
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function doTestCheckMaxLag( $lag ) {
|
|
|
|
|
$mockLB = $this->getMockBuilder( LoadBalancer::class )
|
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
|
->setMethods( [ 'getMaxLag', '__destruct' ] )
|
|
|
|
|
->getMock();
|
|
|
|
|
$mockLB->method( 'getMaxLag' )->willReturn( [ 'somehost', $lag ] );
|
|
|
|
|
$this->setService( 'DBLoadBalancer', $mockLB );
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest();
|
|
|
|
|
|
|
|
|
|
$api = new ApiMain( $req );
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $api );
|
|
|
|
|
|
|
|
|
|
$mockModule = $this->createMock( ApiBase::class );
|
|
|
|
|
$mockModule->method( 'shouldCheckMaxLag' )->willReturn( true );
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$wrapper->checkMaxLag( $mockModule, [ 'maxlag' => 3 ] );
|
|
|
|
|
} finally {
|
|
|
|
|
if ( $lag > 3 ) {
|
|
|
|
|
$this->assertSame( '5', $req->response()->getHeader( 'Retry-After' ) );
|
|
|
|
|
$this->assertSame( (string)$lag, $req->response()->getHeader( 'X-Database-Lag' ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckMaxLagOkay() {
|
|
|
|
|
$this->doTestCheckMaxLag( 3 );
|
|
|
|
|
|
|
|
|
|
// No exception, we're happy
|
|
|
|
|
$this->assertTrue( true );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckMaxLagExceeded() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'Waiting for a database server: 4 seconds lagged.' );
|
|
|
|
|
|
|
|
|
|
$this->setMwGlobals( 'wgShowHostnames', false );
|
|
|
|
|
|
|
|
|
|
$this->doTestCheckMaxLag( 4 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckMaxLagExceededWithHostNames() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'Waiting for somehost: 4 seconds lagged.' );
|
|
|
|
|
|
|
|
|
|
$this->setMwGlobals( 'wgShowHostnames', true );
|
|
|
|
|
|
|
|
|
|
$this->doTestCheckMaxLag( 4 );
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-19 01:13:16 +00:00
|
|
|
public static function provideAssert() {
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
|
|
|
|
[ false, [], 'user', 'assertuserfailed' ],
|
|
|
|
|
[ true, [], 'user', false ],
|
|
|
|
|
[ true, [], 'bot', 'assertbotfailed' ],
|
|
|
|
|
[ true, [ 'bot' ], 'user', false ],
|
|
|
|
|
[ true, [ 'bot' ], 'bot', false ],
|
|
|
|
|
];
|
2014-01-19 01:13:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests the assert={user|bot} functionality
|
|
|
|
|
*
|
|
|
|
|
* @dataProvider provideAssert
|
2015-03-29 08:41:38 +00:00
|
|
|
* @param bool $registered
|
|
|
|
|
* @param array $rights
|
2014-01-19 01:13:16 +00:00
|
|
|
* @param string $assert
|
2014-07-24 12:55:43 +00:00
|
|
|
* @param string|bool $error False if no error expected
|
2014-01-19 01:13:16 +00:00
|
|
|
*/
|
2015-03-29 08:41:38 +00:00
|
|
|
public function testAssert( $registered, $rights, $assert, $error ) {
|
|
|
|
|
if ( $registered ) {
|
2017-05-10 02:25:56 +00:00
|
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
|
|
|
$user->load(); // load before setting mRights
|
|
|
|
|
} else {
|
|
|
|
|
$user = new User();
|
2015-03-29 08:41:38 +00:00
|
|
|
}
|
2019-04-09 06:58:04 +00:00
|
|
|
$this->overrideUserPermissions( $user, $rights );
|
2014-01-19 01:13:16 +00:00
|
|
|
try {
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->doApiRequest( [
|
2014-01-19 01:13:16 +00:00
|
|
|
'action' => 'query',
|
|
|
|
|
'assert' => $assert,
|
2016-02-17 09:09:32 +00:00
|
|
|
], null, null, $user );
|
2014-01-19 01:13:16 +00:00
|
|
|
$this->assertFalse( $error ); // That no error was expected
|
2016-10-19 16:54:25 +00:00
|
|
|
} catch ( ApiUsageException $e ) {
|
2017-05-10 02:25:56 +00:00
|
|
|
$this->assertTrue( self::apiExceptionHasCode( $e, $error ),
|
|
|
|
|
"Error '{$e->getMessage()}' matched expected '$error'" );
|
2014-01-19 01:13:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-05 14:35:47 +00:00
|
|
|
/**
|
|
|
|
|
* Tests the assertuser= functionality
|
|
|
|
|
*/
|
|
|
|
|
public function testAssertUser() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$this->doApiRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'assertuser' => $user->getName(),
|
|
|
|
|
], null, null, $user );
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$this->doApiRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'assertuser' => $user->getName() . 'X',
|
|
|
|
|
], null, null, $user );
|
|
|
|
|
$this->fail( 'Expected exception not thrown' );
|
2016-10-19 16:54:25 +00:00
|
|
|
} catch ( ApiUsageException $e ) {
|
|
|
|
|
$this->assertTrue( self::apiExceptionHasCode( $e, 'assertnameduserfailed' ) );
|
2016-10-05 14:35:47 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-20 14:32:03 +00:00
|
|
|
/**
|
|
|
|
|
* Test that 'assert' is processed before module errors
|
|
|
|
|
*/
|
|
|
|
|
public function testAssertBeforeModule() {
|
|
|
|
|
// Sanity check that the query without assert throws too-many-titles
|
|
|
|
|
try {
|
|
|
|
|
$this->doApiRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'titles' => implode( '|', range( 1, ApiBase::LIMIT_SML1 + 1 ) ),
|
|
|
|
|
], null, null, new User );
|
|
|
|
|
$this->fail( 'Expected exception not thrown' );
|
|
|
|
|
} catch ( ApiUsageException $e ) {
|
|
|
|
|
$this->assertTrue( self::apiExceptionHasCode( $e, 'too-many-titles' ), 'sanity check' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now test that the assert happens first
|
|
|
|
|
try {
|
|
|
|
|
$this->doApiRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'titles' => implode( '|', range( 1, ApiBase::LIMIT_SML1 + 1 ) ),
|
|
|
|
|
'assert' => 'user',
|
|
|
|
|
], null, null, new User );
|
|
|
|
|
$this->fail( 'Expected exception not thrown' );
|
|
|
|
|
} catch ( ApiUsageException $e ) {
|
|
|
|
|
$this->assertTrue( self::apiExceptionHasCode( $e, 'assertuserfailed' ),
|
|
|
|
|
"Error '{$e->getMessage()}' matched expected 'assertuserfailed'" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-17 19:08:10 +00:00
|
|
|
/**
|
|
|
|
|
* Test if all classes in the main module manager exists
|
|
|
|
|
*/
|
|
|
|
|
public function testClassNamesInModuleManager() {
|
|
|
|
|
$api = new ApiMain(
|
2016-02-17 09:09:32 +00:00
|
|
|
new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] )
|
2014-11-17 19:08:10 +00:00
|
|
|
);
|
|
|
|
|
$modules = $api->getModuleManager()->getNamesWithClasses();
|
2016-12-21 15:06:57 +00:00
|
|
|
|
2015-06-17 20:01:00 +00:00
|
|
|
foreach ( $modules as $name => $class ) {
|
2016-12-21 15:06:57 +00:00
|
|
|
$this->assertTrue(
|
|
|
|
|
class_exists( $class ),
|
|
|
|
|
'Class ' . $class . ' for api module ' . $name . ' does not exist (with exact case)'
|
2014-11-17 19:08:10 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test HTTP precondition headers
|
|
|
|
|
*
|
|
|
|
|
* @dataProvider provideCheckConditionalRequestHeaders
|
|
|
|
|
* @param array $headers HTTP headers
|
|
|
|
|
* @param array $conditions Return data for ApiBase::getConditionalRequestData
|
|
|
|
|
* @param int $status Expected response status
|
2018-03-26 14:38:41 +00:00
|
|
|
* @param array $options Array of options:
|
|
|
|
|
* post => true Request is a POST
|
2017-11-01 20:55:24 +00:00
|
|
|
* cdn => true CDN is enabled ($wgUseCdn)
|
2015-08-17 20:52:09 +00:00
|
|
|
*/
|
2015-10-03 13:44:13 +00:00
|
|
|
public function testCheckConditionalRequestHeaders(
|
2018-03-26 14:38:41 +00:00
|
|
|
$headers, $conditions, $status, $options = []
|
2015-10-03 13:44:13 +00:00
|
|
|
) {
|
2018-03-26 14:38:41 +00:00
|
|
|
$request = new FauxRequest(
|
|
|
|
|
[ 'action' => 'query', 'meta' => 'siteinfo' ],
|
|
|
|
|
!empty( $options['post'] )
|
|
|
|
|
);
|
2015-08-17 20:52:09 +00:00
|
|
|
$request->setHeaders( $headers );
|
|
|
|
|
$request->response()->statusHeader( 200 ); // Why doesn't it default?
|
|
|
|
|
|
2015-09-22 14:33:24 +00:00
|
|
|
$context = $this->apiContext->newTestContext( $request, null );
|
|
|
|
|
$api = new ApiMain( $context );
|
2015-08-17 20:52:09 +00:00
|
|
|
$priv = TestingAccessWrapper::newFromObject( $api );
|
|
|
|
|
$priv->mInternalMode = false;
|
|
|
|
|
|
2018-03-26 14:38:41 +00:00
|
|
|
if ( !empty( $options['cdn'] ) ) {
|
2017-11-01 20:55:24 +00:00
|
|
|
$this->setMwGlobals( 'wgUseCdn', true );
|
2018-03-26 14:38:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Can't do this in TestSetup.php because Setup.php will override it
|
|
|
|
|
$this->setMwGlobals( 'wgCacheEpoch', '20030516000000' );
|
|
|
|
|
|
2018-01-13 00:02:09 +00:00
|
|
|
$module = $this->getMockBuilder( ApiBase::class )
|
2016-02-17 09:09:32 +00:00
|
|
|
->setConstructorArgs( [ $api, 'mock' ] )
|
|
|
|
|
->setMethods( [ 'getConditionalRequestData' ] )
|
2015-08-17 20:52:09 +00:00
|
|
|
->getMockForAbstractClass();
|
|
|
|
|
$module->expects( $this->any() )
|
|
|
|
|
->method( 'getConditionalRequestData' )
|
|
|
|
|
->will( $this->returnCallback( function ( $condition ) use ( $conditions ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
return $conditions[$condition] ?? null;
|
2015-08-17 20:52:09 +00:00
|
|
|
} ) );
|
|
|
|
|
|
|
|
|
|
$ret = $priv->checkConditionalRequestHeaders( $module );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $status, $request->response()->getStatusCode() );
|
|
|
|
|
$this->assertSame( $status === 200, $ret );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function provideCheckConditionalRequestHeaders() {
|
2017-11-01 20:55:24 +00:00
|
|
|
global $wgCdnMaxAge;
|
2015-08-17 20:52:09 +00:00
|
|
|
$now = time();
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
2015-08-17 20:52:09 +00:00
|
|
|
// Non-existing from module is ignored
|
2018-03-26 14:38:41 +00:00
|
|
|
'If-None-Match' => [ [ 'If-None-Match' => '"foo", "bar"' ], [], 200 ],
|
|
|
|
|
'If-Modified-Since' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => 'Tue, 18 Aug 2015 00:00:00 GMT' ], [], 200 ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// No headers
|
2018-03-26 14:38:41 +00:00
|
|
|
'No headers' => [ [], [ 'etag' => '""', 'last-modified' => '20150815000000', ], 200 ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// Basic If-None-Match
|
2018-03-26 14:38:41 +00:00
|
|
|
'If-None-Match with matching etag' =>
|
|
|
|
|
[ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"bar"' ], 304 ],
|
|
|
|
|
'If-None-Match with non-matching etag' =>
|
|
|
|
|
[ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"baz"' ], 200 ],
|
|
|
|
|
'Strong If-None-Match with weak matching etag' =>
|
|
|
|
|
[ [ 'If-None-Match' => '"foo"' ], [ 'etag' => 'W/"foo"' ], 304 ],
|
|
|
|
|
'Weak If-None-Match with strong matching etag' =>
|
|
|
|
|
[ [ 'If-None-Match' => 'W/"foo"' ], [ 'etag' => '"foo"' ], 304 ],
|
|
|
|
|
'Weak If-None-Match with weak matching etag' =>
|
|
|
|
|
[ [ 'If-None-Match' => 'W/"foo"' ], [ 'etag' => 'W/"foo"' ], 304 ],
|
|
|
|
|
|
|
|
|
|
// Pointless for GET, but supported
|
|
|
|
|
'If-None-Match: *' => [ [ 'If-None-Match' => '*' ], [], 304 ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// Basic If-Modified-Since
|
2018-03-26 14:38:41 +00:00
|
|
|
'If-Modified-Since, modified one second earlier' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
|
|
|
|
|
'If-Modified-Since, modified now' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now ) ], 304 ],
|
|
|
|
|
'If-Modified-Since, modified one second later' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now + 1 ) ], 200 ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// If-Modified-Since ignored when If-None-Match is given too
|
2018-03-26 14:38:41 +00:00
|
|
|
'Non-matching If-None-Match and matching If-Modified-Since' =>
|
|
|
|
|
[ [ 'If-None-Match' => '""',
|
|
|
|
|
'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ],
|
|
|
|
|
[ 'etag' => '"x"', 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ],
|
|
|
|
|
'Non-matching If-None-Match and matching If-Modified-Since with no ETag' =>
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'If-None-Match' => '""',
|
|
|
|
|
'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now )
|
|
|
|
|
],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ],
|
|
|
|
|
304
|
|
|
|
|
],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// Ignored for POST
|
2018-03-26 14:38:41 +00:00
|
|
|
'Matching If-None-Match with POST' =>
|
|
|
|
|
[ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"bar"' ], 200,
|
|
|
|
|
[ 'post' => true ] ],
|
|
|
|
|
'Matching If-Modified-Since with POST' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200,
|
|
|
|
|
[ 'post' => true ] ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// Other date formats allowed by the RFC
|
2018-03-26 14:38:41 +00:00
|
|
|
'If-Modified-Since with alternate date format 1' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => gmdate( 'l, d-M-y H:i:s', $now ) . ' GMT' ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
|
|
|
|
|
'If-Modified-Since with alternate date format 2' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => gmdate( 'D M j H:i:s Y', $now ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// Old browser extension to HTTP/1.0
|
2018-03-26 14:38:41 +00:00
|
|
|
'If-Modified-Since with length' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) . '; length=123' ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
|
2015-08-17 20:52:09 +00:00
|
|
|
|
|
|
|
|
// Invalid date formats should be ignored
|
2018-03-26 14:38:41 +00:00
|
|
|
'If-Modified-Since with invalid date format' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => gmdate( 'Y-m-d H:i:s', $now ) . ' GMT' ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ],
|
|
|
|
|
'If-Modified-Since with entirely unparseable date' =>
|
|
|
|
|
[ [ 'If-Modified-Since' => 'a potato' ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ],
|
|
|
|
|
|
2017-11-01 20:55:24 +00:00
|
|
|
// Anything before $wgCdnMaxAge seconds ago should be considered
|
2018-03-26 14:38:41 +00:00
|
|
|
// expired.
|
|
|
|
|
'If-Modified-Since with CDN post-expiry' =>
|
2017-11-01 20:55:24 +00:00
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now - $wgCdnMaxAge * 2 ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - $wgCdnMaxAge * 3 ) ],
|
2018-03-26 14:38:41 +00:00
|
|
|
200, [ 'cdn' => true ] ],
|
|
|
|
|
'If-Modified-Since with CDN pre-expiry' =>
|
2017-11-01 20:55:24 +00:00
|
|
|
[ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now - $wgCdnMaxAge / 2 ) ],
|
|
|
|
|
[ 'last-modified' => wfTimestamp( TS_MW, $now - $wgCdnMaxAge * 3 ) ],
|
2018-03-26 14:38:41 +00:00
|
|
|
304, [ 'cdn' => true ] ],
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2015-08-17 20:52:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test conditional headers output
|
|
|
|
|
* @dataProvider provideConditionalRequestHeadersOutput
|
|
|
|
|
* @param array $conditions Return data for ApiBase::getConditionalRequestData
|
|
|
|
|
* @param array $headers Expected output headers
|
|
|
|
|
* @param bool $isError $isError flag
|
|
|
|
|
* @param bool $post Request is a POST
|
|
|
|
|
*/
|
2015-10-03 13:44:13 +00:00
|
|
|
public function testConditionalRequestHeadersOutput(
|
|
|
|
|
$conditions, $headers, $isError = false, $post = false
|
|
|
|
|
) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$request = new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ], $post );
|
2015-08-17 20:52:09 +00:00
|
|
|
$response = $request->response();
|
|
|
|
|
|
|
|
|
|
$api = new ApiMain( $request );
|
|
|
|
|
$priv = TestingAccessWrapper::newFromObject( $api );
|
|
|
|
|
$priv->mInternalMode = false;
|
|
|
|
|
|
2018-01-13 00:02:09 +00:00
|
|
|
$module = $this->getMockBuilder( ApiBase::class )
|
2016-02-17 09:09:32 +00:00
|
|
|
->setConstructorArgs( [ $api, 'mock' ] )
|
|
|
|
|
->setMethods( [ 'getConditionalRequestData' ] )
|
2015-08-17 20:52:09 +00:00
|
|
|
->getMockForAbstractClass();
|
|
|
|
|
$module->expects( $this->any() )
|
|
|
|
|
->method( 'getConditionalRequestData' )
|
|
|
|
|
->will( $this->returnCallback( function ( $condition ) use ( $conditions ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
return $conditions[$condition] ?? null;
|
2015-08-17 20:52:09 +00:00
|
|
|
} ) );
|
|
|
|
|
$priv->mModule = $module;
|
|
|
|
|
|
|
|
|
|
$priv->sendCacheHeaders( $isError );
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
foreach ( [ 'Last-Modified', 'ETag' ] as $header ) {
|
2015-08-17 20:52:09 +00:00
|
|
|
$this->assertEquals(
|
2017-10-06 22:17:58 +00:00
|
|
|
$headers[$header] ?? null,
|
2015-08-17 20:52:09 +00:00
|
|
|
$response->getHeader( $header ),
|
|
|
|
|
$header
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function provideConditionalRequestHeadersOutput() {
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
[],
|
|
|
|
|
[]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'etag' => '"foo"' ],
|
|
|
|
|
[ 'ETag' => '"foo"' ]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'last-modified' => '20150818000102' ],
|
|
|
|
|
[ 'Last-Modified' => 'Tue, 18 Aug 2015 00:01:02 GMT' ]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'etag' => '"foo"', 'last-modified' => '20150818000102' ],
|
|
|
|
|
[ 'ETag' => '"foo"', 'Last-Modified' => 'Tue, 18 Aug 2015 00:01:02 GMT' ]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'etag' => '"foo"', 'last-modified' => '20150818000102' ],
|
|
|
|
|
[],
|
2015-08-17 20:52:09 +00:00
|
|
|
true,
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'etag' => '"foo"', 'last-modified' => '20150818000102' ],
|
|
|
|
|
[],
|
2015-08-17 20:52:09 +00:00
|
|
|
false,
|
|
|
|
|
true,
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
|
|
|
|
];
|
2015-08-17 20:52:09 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-26 14:38:41 +00:00
|
|
|
public function testCheckExecutePermissionsReadProhibited() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'You need read permission to use this module.' );
|
|
|
|
|
|
|
|
|
|
$this->setGroupPermissions( '*', 'read', false );
|
|
|
|
|
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) );
|
|
|
|
|
$main->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckExecutePermissionWriteDisabled() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'Editing of this wiki through the API is disabled. Make sure the ' .
|
|
|
|
|
'"$wgEnableWriteAPI=true;" statement is included in the wiki\'s ' .
|
|
|
|
|
'"LocalSettings.php" file.' );
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [
|
|
|
|
|
'action' => 'edit',
|
|
|
|
|
'title' => 'Some page',
|
|
|
|
|
'text' => 'Some text',
|
|
|
|
|
'token' => '+\\',
|
|
|
|
|
] ) );
|
|
|
|
|
$main->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckExecutePermissionWriteApiProhibited() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
"You're not allowed to edit this wiki through the API." );
|
|
|
|
|
$this->setGroupPermissions( '*', 'writeapi', false );
|
|
|
|
|
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [
|
|
|
|
|
'action' => 'edit',
|
|
|
|
|
'title' => 'Some page',
|
|
|
|
|
'text' => 'Some text',
|
|
|
|
|
'token' => '+\\',
|
|
|
|
|
] ), /* enableWrite = */ true );
|
|
|
|
|
$main->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckExecutePermissionPromiseNonWrite() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class,
|
|
|
|
|
'The "Promise-Non-Write-API-Action" HTTP header cannot be sent ' .
|
|
|
|
|
'to write-mode API modules.' );
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'action' => 'edit',
|
|
|
|
|
'title' => 'Some page',
|
|
|
|
|
'text' => 'Some text',
|
|
|
|
|
'token' => '+\\',
|
|
|
|
|
] );
|
|
|
|
|
$req->setHeaders( [ 'Promise-Non-Write-API-Action' => '1' ] );
|
|
|
|
|
$main = new ApiMain( $req, /* enableWrite = */ true );
|
|
|
|
|
$main->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckExecutePermissionHookAbort() {
|
|
|
|
|
$this->setExpectedException( ApiUsageException::class, 'Main Page' );
|
|
|
|
|
|
|
|
|
|
$this->setTemporaryHook( 'ApiCheckCanExecute', function ( $unused1, $unused2, &$message ) {
|
|
|
|
|
$message = 'mainpage';
|
|
|
|
|
return false;
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [
|
|
|
|
|
'action' => 'edit',
|
|
|
|
|
'title' => 'Some page',
|
|
|
|
|
'text' => 'Some text',
|
|
|
|
|
'token' => '+\\',
|
|
|
|
|
] ), /* enableWrite = */ true );
|
|
|
|
|
$main->execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGetValUnsupportedArray() {
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
'siprop' => [ 'general', 'namespaces' ],
|
|
|
|
|
] ) );
|
|
|
|
|
$this->assertSame( 'myDefault', $main->getVal( 'siprop', 'myDefault' ) );
|
|
|
|
|
$main->execute();
|
|
|
|
|
$this->assertSame( 'Parameter "siprop" uses unsupported PHP array syntax.',
|
|
|
|
|
$main->getResult()->getResultData()['warnings']['main']['warnings'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testReportUnusedParams() {
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [
|
|
|
|
|
'action' => 'query',
|
|
|
|
|
'meta' => 'siteinfo',
|
|
|
|
|
'unusedparam' => 'unusedval',
|
|
|
|
|
'anotherunusedparam' => 'anotherval',
|
|
|
|
|
] ) );
|
|
|
|
|
$main->execute();
|
|
|
|
|
$this->assertSame( 'Unrecognized parameters: unusedparam, anotherunusedparam.',
|
|
|
|
|
$main->getResult()->getResultData()['warnings']['main']['warnings'] );
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-08 14:20:30 +00:00
|
|
|
public function testLacksSameOriginSecurity() {
|
|
|
|
|
// Basic test
|
2016-05-20 18:11:58 +00:00
|
|
|
$main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) );
|
2015-05-08 14:20:30 +00:00
|
|
|
$this->assertFalse( $main->lacksSameOriginSecurity(), 'Basic test, should have security' );
|
|
|
|
|
|
|
|
|
|
// JSONp
|
|
|
|
|
$main = new ApiMain(
|
2016-05-20 18:11:58 +00:00
|
|
|
new FauxRequest( [ 'action' => 'query', 'format' => 'xml', 'callback' => 'foo' ] )
|
2015-05-08 14:20:30 +00:00
|
|
|
);
|
|
|
|
|
$this->assertTrue( $main->lacksSameOriginSecurity(), 'JSONp, should lack security' );
|
|
|
|
|
|
|
|
|
|
// Header
|
2016-05-20 18:11:58 +00:00
|
|
|
$request = new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] );
|
2015-05-08 14:20:30 +00:00
|
|
|
$request->setHeader( 'TrEaT-As-UnTrUsTeD', '' ); // With falsey value!
|
|
|
|
|
$main = new ApiMain( $request );
|
|
|
|
|
$this->assertTrue( $main->lacksSameOriginSecurity(), 'Header supplied, should lack security' );
|
|
|
|
|
|
|
|
|
|
// Hook
|
2016-05-20 18:11:58 +00:00
|
|
|
$this->mergeMwGlobalArrayValue( 'wgHooks', [
|
|
|
|
|
'RequestHasSameOriginSecurity' => [ function () {
|
|
|
|
|
return false;
|
|
|
|
|
} ]
|
|
|
|
|
] );
|
|
|
|
|
$main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) );
|
2015-05-08 14:20:30 +00:00
|
|
|
$this->assertTrue( $main->lacksSameOriginSecurity(), 'Hook, should lack security' );
|
|
|
|
|
}
|
2016-10-19 16:54:25 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test proper creation of the ApiErrorFormatter
|
2018-03-26 14:38:41 +00:00
|
|
|
*
|
2016-10-19 16:54:25 +00:00
|
|
|
* @dataProvider provideApiErrorFormatterCreation
|
|
|
|
|
* @param array $request Request parameters
|
|
|
|
|
* @param array $expect Expected data
|
|
|
|
|
* - uselang: ApiMain language
|
|
|
|
|
* - class: ApiErrorFormatter class
|
|
|
|
|
* - lang: ApiErrorFormatter language
|
|
|
|
|
* - format: ApiErrorFormatter format
|
|
|
|
|
* - usedb: ApiErrorFormatter use-database flag
|
|
|
|
|
*/
|
|
|
|
|
public function testApiErrorFormatterCreation( array $request, array $expect ) {
|
|
|
|
|
$context = new RequestContext();
|
|
|
|
|
$context->setRequest( new FauxRequest( $request ) );
|
|
|
|
|
$context->setLanguage( 'ru' );
|
|
|
|
|
|
|
|
|
|
$main = new ApiMain( $context );
|
|
|
|
|
$formatter = $main->getErrorFormatter();
|
|
|
|
|
$wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expect['uselang'], $main->getLanguage()->getCode() );
|
|
|
|
|
$this->assertInstanceOf( $expect['class'], $formatter );
|
|
|
|
|
$this->assertSame( $expect['lang'], $formatter->getLanguage()->getCode() );
|
|
|
|
|
$this->assertSame( $expect['format'], $wrappedFormatter->format );
|
|
|
|
|
$this->assertSame( $expect['usedb'], $wrappedFormatter->useDB );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function provideApiErrorFormatterCreation() {
|
|
|
|
|
return [
|
|
|
|
|
'Default (BC)' => [ [], [
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter_BackCompat::class,
|
|
|
|
|
'lang' => 'en',
|
|
|
|
|
'format' => 'none',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
] ],
|
|
|
|
|
'BC ignores fields' => [ [ 'errorlang' => 'de', 'errorsuselocal' => 1 ], [
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter_BackCompat::class,
|
|
|
|
|
'lang' => 'en',
|
|
|
|
|
'format' => 'none',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
] ],
|
|
|
|
|
'Explicit BC' => [ [ 'errorformat' => 'bc' ], [
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter_BackCompat::class,
|
|
|
|
|
'lang' => 'en',
|
|
|
|
|
'format' => 'none',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
] ],
|
|
|
|
|
'Basic' => [ [ 'errorformat' => 'wikitext' ], [
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
|
|
|
|
'lang' => 'ru',
|
|
|
|
|
'format' => 'wikitext',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
] ],
|
|
|
|
|
'Follows uselang' => [ [ 'uselang' => 'fr', 'errorformat' => 'plaintext' ], [
|
|
|
|
|
'uselang' => 'fr',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
|
|
|
|
'lang' => 'fr',
|
|
|
|
|
'format' => 'plaintext',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
] ],
|
|
|
|
|
'Explicitly follows uselang' => [
|
|
|
|
|
[ 'uselang' => 'fr', 'errorlang' => 'uselang', 'errorformat' => 'plaintext' ],
|
|
|
|
|
[
|
|
|
|
|
'uselang' => 'fr',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
|
|
|
|
'lang' => 'fr',
|
|
|
|
|
'format' => 'plaintext',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'uselang=content' => [
|
|
|
|
|
[ 'uselang' => 'content', 'errorformat' => 'plaintext' ],
|
|
|
|
|
[
|
2017-04-03 13:41:21 +00:00
|
|
|
'uselang' => 'en',
|
2016-10-19 16:54:25 +00:00
|
|
|
'class' => ApiErrorFormatter::class,
|
2017-04-03 13:41:21 +00:00
|
|
|
'lang' => 'en',
|
2016-10-19 16:54:25 +00:00
|
|
|
'format' => 'plaintext',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'errorlang=content' => [
|
|
|
|
|
[ 'errorlang' => 'content', 'errorformat' => 'plaintext' ],
|
|
|
|
|
[
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
2017-04-03 13:41:21 +00:00
|
|
|
'lang' => 'en',
|
2016-10-19 16:54:25 +00:00
|
|
|
'format' => 'plaintext',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'Explicit parameters' => [
|
|
|
|
|
[ 'errorlang' => 'de', 'errorformat' => 'html', 'errorsuselocal' => 1 ],
|
|
|
|
|
[
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
|
|
|
|
'lang' => 'de',
|
|
|
|
|
'format' => 'html',
|
|
|
|
|
'usedb' => true,
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'Explicit parameters override uselang' => [
|
|
|
|
|
[ 'errorlang' => 'de', 'uselang' => 'fr', 'errorformat' => 'raw' ],
|
|
|
|
|
[
|
|
|
|
|
'uselang' => 'fr',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
|
|
|
|
'lang' => 'de',
|
|
|
|
|
'format' => 'raw',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'Bogus language doesn\'t explode' => [
|
|
|
|
|
[ 'errorlang' => '<bogus1>', 'uselang' => '<bogus2>', 'errorformat' => 'none' ],
|
|
|
|
|
[
|
|
|
|
|
'uselang' => 'en',
|
|
|
|
|
'class' => ApiErrorFormatter::class,
|
|
|
|
|
'lang' => 'en',
|
|
|
|
|
'format' => 'none',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'Bogus format doesn\'t explode' => [ [ 'errorformat' => 'bogus' ], [
|
|
|
|
|
'uselang' => 'ru',
|
|
|
|
|
'class' => ApiErrorFormatter_BackCompat::class,
|
|
|
|
|
'lang' => 'en',
|
|
|
|
|
'format' => 'none',
|
|
|
|
|
'usedb' => false,
|
|
|
|
|
] ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideExceptionErrors
|
|
|
|
|
* @param Exception $exception
|
|
|
|
|
* @param array $expectReturn
|
|
|
|
|
* @param array $expectResult
|
|
|
|
|
*/
|
|
|
|
|
public function testExceptionErrors( $error, $expectReturn, $expectResult ) {
|
|
|
|
|
$context = new RequestContext();
|
|
|
|
|
$context->setRequest( new FauxRequest( [ 'errorformat' => 'plaintext' ] ) );
|
|
|
|
|
$context->setLanguage( 'en' );
|
|
|
|
|
$context->setConfig( new MultiConfig( [
|
2017-02-22 01:27:17 +00:00
|
|
|
new HashConfig( [
|
2018-07-17 16:51:36 +00:00
|
|
|
'ShowHostnames' => true, 'ShowExceptionDetails' => true,
|
2017-02-22 01:27:17 +00:00
|
|
|
] ),
|
2016-10-19 16:54:25 +00:00
|
|
|
$context->getConfig()
|
|
|
|
|
] ) );
|
|
|
|
|
|
|
|
|
|
$main = new ApiMain( $context );
|
|
|
|
|
$main->addWarning( new RawMessage( 'existing warning' ), 'existing-warning' );
|
|
|
|
|
$main->addError( new RawMessage( 'existing error' ), 'existing-error' );
|
|
|
|
|
|
|
|
|
|
$ret = TestingAccessWrapper::newFromObject( $main )->substituteResultWithError( $error );
|
|
|
|
|
$this->assertSame( $expectReturn, $ret );
|
|
|
|
|
|
|
|
|
|
// PHPUnit sometimes adds some SplObjectStorage garbage to the arrays,
|
|
|
|
|
// so let's try ->assertEquals().
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
$expectResult,
|
|
|
|
|
$main->getResult()->getResultData( [], [ 'Strip' => 'all' ] )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-05 23:39:50 +00:00
|
|
|
// Not static so $this can be used
|
2016-10-19 16:54:25 +00:00
|
|
|
public function provideExceptionErrors() {
|
|
|
|
|
$reqId = WebRequest::getRequestId();
|
|
|
|
|
$doclink = wfExpandUrl( wfScript( 'api' ) );
|
|
|
|
|
|
|
|
|
|
$ex = new InvalidArgumentException( 'Random exception' );
|
|
|
|
|
$trace = wfMessage( 'api-exception-trace',
|
|
|
|
|
get_class( $ex ),
|
|
|
|
|
$ex->getFile(),
|
|
|
|
|
$ex->getLine(),
|
|
|
|
|
MWExceptionHandler::getRedactedTraceAsString( $ex )
|
|
|
|
|
)->inLanguage( 'en' )->useDatabase( false )->text();
|
|
|
|
|
|
2017-04-05 23:39:50 +00:00
|
|
|
$dbex = new DBQueryError(
|
2018-01-13 00:02:09 +00:00
|
|
|
$this->createMock( \Wikimedia\Rdbms\IDatabase::class ),
|
2017-04-05 23:39:50 +00:00
|
|
|
'error', 1234, 'SELECT 1', __METHOD__ );
|
2016-10-19 16:54:25 +00:00
|
|
|
$dbtrace = wfMessage( 'api-exception-trace',
|
|
|
|
|
get_class( $dbex ),
|
|
|
|
|
$dbex->getFile(),
|
|
|
|
|
$dbex->getLine(),
|
|
|
|
|
MWExceptionHandler::getRedactedTraceAsString( $dbex )
|
|
|
|
|
)->inLanguage( 'en' )->useDatabase( false )->text();
|
|
|
|
|
|
2018-11-07 16:25:40 +00:00
|
|
|
// The specific exception doesn't matter, as long as it's namespaced.
|
|
|
|
|
$nsex = new MediaWiki\ShellDisabledError();
|
|
|
|
|
$nstrace = wfMessage( 'api-exception-trace',
|
|
|
|
|
get_class( $nsex ),
|
|
|
|
|
$nsex->getFile(),
|
|
|
|
|
$nsex->getLine(),
|
|
|
|
|
MWExceptionHandler::getRedactedTraceAsString( $nsex )
|
|
|
|
|
)->inLanguage( 'en' )->useDatabase( false )->text();
|
|
|
|
|
|
2016-10-19 16:54:25 +00:00
|
|
|
$apiEx1 = new ApiUsageException( null,
|
|
|
|
|
StatusValue::newFatal( new ApiRawMessage( 'An error', 'sv-error1' ) ) );
|
|
|
|
|
TestingAccessWrapper::newFromObject( $apiEx1 )->modulePath = 'foo+bar';
|
|
|
|
|
$apiEx1->getStatusValue()->warning( new ApiRawMessage( 'A warning', 'sv-warn1' ) );
|
|
|
|
|
$apiEx1->getStatusValue()->warning( new ApiRawMessage( 'Another warning', 'sv-warn2' ) );
|
|
|
|
|
$apiEx1->getStatusValue()->fatal( new ApiRawMessage( 'Another error', 'sv-error2' ) );
|
|
|
|
|
|
2018-11-07 16:25:40 +00:00
|
|
|
$badMsg = $this->getMockBuilder( ApiRawMessage::class )
|
|
|
|
|
->setConstructorArgs( [ 'An error', 'ignored' ] )
|
|
|
|
|
->setMethods( [ 'getApiCode' ] )
|
|
|
|
|
->getMock();
|
|
|
|
|
$badMsg->method( 'getApiCode' )->willReturn( "bad\nvalue" );
|
|
|
|
|
$apiEx2 = new ApiUsageException( null, StatusValue::newFatal( $badMsg ) );
|
|
|
|
|
|
2016-10-19 16:54:25 +00:00
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
$ex,
|
|
|
|
|
[ 'existing-error', 'internal_api_error_InvalidArgumentException' ],
|
|
|
|
|
[
|
|
|
|
|
'warnings' => [
|
|
|
|
|
[ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
|
|
|
|
|
],
|
|
|
|
|
'errors' => [
|
|
|
|
|
[ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
|
|
|
|
|
[
|
|
|
|
|
'code' => 'internal_api_error_InvalidArgumentException',
|
|
|
|
|
'text' => "[$reqId] Exception caught: Random exception",
|
2018-11-26 18:31:41 +00:00
|
|
|
'data' => [
|
|
|
|
|
'errorclass' => InvalidArgumentException::class,
|
|
|
|
|
],
|
2016-10-19 16:54:25 +00:00
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'trace' => $trace,
|
|
|
|
|
'servedby' => wfHostname(),
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
$dbex,
|
|
|
|
|
[ 'existing-error', 'internal_api_error_DBQueryError' ],
|
|
|
|
|
[
|
|
|
|
|
'warnings' => [
|
|
|
|
|
[ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
|
|
|
|
|
],
|
|
|
|
|
'errors' => [
|
|
|
|
|
[ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
|
|
|
|
|
[
|
|
|
|
|
'code' => 'internal_api_error_DBQueryError',
|
2018-07-17 16:51:36 +00:00
|
|
|
'text' => "[$reqId] Exception caught: A database query error has occurred. " .
|
|
|
|
|
"This may indicate a bug in the software.",
|
2018-11-26 18:31:41 +00:00
|
|
|
'data' => [
|
|
|
|
|
'errorclass' => DBQueryError::class,
|
|
|
|
|
],
|
2016-10-19 16:54:25 +00:00
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'trace' => $dbtrace,
|
|
|
|
|
'servedby' => wfHostname(),
|
|
|
|
|
]
|
|
|
|
|
],
|
2018-11-07 16:25:40 +00:00
|
|
|
[
|
|
|
|
|
$nsex,
|
|
|
|
|
[ 'existing-error', 'internal_api_error_MediaWiki\ShellDisabledError' ],
|
|
|
|
|
[
|
|
|
|
|
'warnings' => [
|
|
|
|
|
[ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
|
|
|
|
|
],
|
|
|
|
|
'errors' => [
|
|
|
|
|
[ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
|
|
|
|
|
[
|
|
|
|
|
'code' => 'internal_api_error_MediaWiki\ShellDisabledError',
|
|
|
|
|
'text' => "[$reqId] Exception caught: " . $nsex->getMessage(),
|
2018-11-26 18:31:41 +00:00
|
|
|
'data' => [
|
|
|
|
|
'errorclass' => MediaWiki\ShellDisabledError::class,
|
|
|
|
|
],
|
2018-11-07 16:25:40 +00:00
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'trace' => $nstrace,
|
|
|
|
|
'servedby' => wfHostname(),
|
|
|
|
|
]
|
|
|
|
|
],
|
2016-10-19 16:54:25 +00:00
|
|
|
[
|
|
|
|
|
$apiEx1,
|
|
|
|
|
[ 'existing-error', 'sv-error1', 'sv-error2' ],
|
|
|
|
|
[
|
|
|
|
|
'warnings' => [
|
|
|
|
|
[ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
|
|
|
|
|
[ 'code' => 'sv-warn1', 'text' => 'A warning', 'module' => 'foo+bar' ],
|
|
|
|
|
[ 'code' => 'sv-warn2', 'text' => 'Another warning', 'module' => 'foo+bar' ],
|
|
|
|
|
],
|
|
|
|
|
'errors' => [
|
|
|
|
|
[ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
|
|
|
|
|
[ 'code' => 'sv-error1', 'text' => 'An error', 'module' => 'foo+bar' ],
|
|
|
|
|
[ 'code' => 'sv-error2', 'text' => 'Another error', 'module' => 'foo+bar' ],
|
|
|
|
|
],
|
2017-01-09 23:38:36 +00:00
|
|
|
'docref' => "See $doclink for API usage. Subscribe to the mediawiki-api-announce mailing " .
|
|
|
|
|
"list at <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> " .
|
|
|
|
|
"for notice of API deprecations and breaking changes.",
|
2016-10-19 16:54:25 +00:00
|
|
|
'servedby' => wfHostname(),
|
|
|
|
|
]
|
|
|
|
|
],
|
2018-11-07 16:25:40 +00:00
|
|
|
[
|
|
|
|
|
$apiEx2,
|
|
|
|
|
[ 'existing-error', '<invalid-code>' ],
|
|
|
|
|
[
|
|
|
|
|
'warnings' => [
|
|
|
|
|
[ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
|
|
|
|
|
],
|
|
|
|
|
'errors' => [
|
|
|
|
|
[ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
|
|
|
|
|
[ 'code' => "bad\nvalue", 'text' => 'An error' ],
|
|
|
|
|
],
|
|
|
|
|
'docref' => "See $doclink for API usage. Subscribe to the mediawiki-api-announce mailing " .
|
|
|
|
|
"list at <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> " .
|
|
|
|
|
"for notice of API deprecations and breaking changes.",
|
|
|
|
|
'servedby' => wfHostname(),
|
|
|
|
|
]
|
|
|
|
|
]
|
2016-10-19 16:54:25 +00:00
|
|
|
];
|
|
|
|
|
}
|
2018-07-19 13:24:48 +00:00
|
|
|
|
|
|
|
|
public function testPrinterParameterValidationError() {
|
|
|
|
|
$api = $this->getNonInternalApiMain( [
|
|
|
|
|
'action' => 'query', 'meta' => 'siteinfo', 'format' => 'json', 'formatversion' => 'bogus',
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
|
$api->execute();
|
|
|
|
|
$txt = ob_get_clean();
|
|
|
|
|
|
|
|
|
|
// Test that the actual output is valid JSON, not just the format of the ApiResult.
|
|
|
|
|
$data = FormatJson::decode( $txt, true );
|
|
|
|
|
$this->assertInternalType( 'array', $data );
|
|
|
|
|
$this->assertArrayHasKey( 'error', $data );
|
|
|
|
|
$this->assertArrayHasKey( 'code', $data['error'] );
|
|
|
|
|
$this->assertSame( 'unknown_formatversion', $data['error']['code'] );
|
|
|
|
|
}
|
2013-10-23 15:36:40 +00:00
|
|
|
}
|