summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/api/format
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/api/format')
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php388
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatJsonTest.php129
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatNoneTest.php51
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php139
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php120
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php93
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php123
7 files changed, 1043 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php
new file mode 100644
index 00000000..55f760f6
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php
@@ -0,0 +1,388 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group API
+ * @covers ApiFormatBase
+ */
+class ApiFormatBaseTest extends ApiFormatTestBase {
+
+ protected $printerName = 'mockbase';
+
+ public function getMockFormatter( ApiMain $main = null, $format, $methods = [] ) {
+ if ( $main === null ) {
+ $context = new RequestContext;
+ $context->setRequest( new FauxRequest( [], true ) );
+ $main = new ApiMain( $context );
+ }
+
+ $mock = $this->getMockBuilder( ApiFormatBase::class )
+ ->setConstructorArgs( [ $main, $format ] )
+ ->setMethods( array_unique( array_merge( $methods, [ 'getMimeType', 'execute' ] ) ) )
+ ->getMock();
+ if ( !in_array( 'getMimeType', $methods, true ) ) {
+ $mock->method( 'getMimeType' )->willReturn( 'text/x-mock' );
+ }
+ return $mock;
+ }
+
+ protected function encodeData( array $params, array $data, $options = [] ) {
+ $options += [
+ 'name' => 'mock',
+ 'class' => ApiFormatBase::class,
+ 'factory' => function ( ApiMain $main, $format ) use ( $options ) {
+ $mock = $this->getMockFormatter( $main, $format );
+ $mock->expects( $this->once() )->method( 'execute' )
+ ->willReturnCallback( function () use ( $mock ) {
+ $mock->printText( "Format {$mock->getFormat()}: " );
+ $mock->printText( "<b>ok</b>" );
+ } );
+
+ if ( isset( $options['status'] ) ) {
+ $mock->setHttpStatus( $options['status'] );
+ }
+
+ return $mock;
+ },
+ 'returnPrinter' => true,
+ ];
+
+ $this->setMwGlobals( [
+ 'wgApiFrameOptions' => 'DENY',
+ ] );
+
+ $ret = parent::encodeData( $params, $data, $options );
+ $printer = TestingAccessWrapper::newFromObject( $ret['printer'] );
+ $text = $ret['text'];
+
+ if ( $options['name'] !== 'mockfm' ) {
+ $ct = 'text/x-mock';
+ $file = 'api-result.mock';
+ $status = isset( $options['status'] ) ? $options['status'] : null;
+ } elseif ( isset( $params['wrappedhtml'] ) ) {
+ $ct = 'text/mediawiki-api-prettyprint-wrapped';
+ $file = 'api-result-wrapped.json';
+ $status = null;
+
+ // Replace varying field
+ $text = preg_replace( '/"time":\d+/', '"time":1234', $text );
+ } else {
+ $ct = 'text/html';
+ $file = 'api-result.html';
+ $status = null;
+
+ // Strip OutputPage-generated HTML
+ if ( preg_match( '!<pre class="api-pretty-content">.*</pre>!s', $text, $m ) ) {
+ $text = $m[0];
+ }
+ }
+
+ $response = $printer->getMain()->getRequest()->response();
+ $this->assertSame( "$ct; charset=utf-8", strtolower( $response->getHeader( 'Content-Type' ) ) );
+ $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) );
+ $this->assertSame( $file, $printer->getFilename() );
+ $this->assertSame( "inline; filename=$file", $response->getHeader( 'Content-Disposition' ) );
+ $this->assertSame( $status, $response->getStatusCode() );
+
+ return $text;
+ }
+
+ public static function provideGeneralEncoding() {
+ return [
+ 'normal' => [
+ [],
+ "Format MOCK: <b>ok</b>",
+ [],
+ [ 'name' => 'mock' ]
+ ],
+ 'normal ignores wrappedhtml' => [
+ [],
+ "Format MOCK: <b>ok</b>",
+ [ 'wrappedhtml' => 1 ],
+ [ 'name' => 'mock' ]
+ ],
+ 'HTML format' => [
+ [],
+ '<pre class="api-pretty-content">Format MOCK: &lt;b>ok&lt;/b></pre>',
+ [],
+ [ 'name' => 'mockfm' ]
+ ],
+ 'wrapped HTML format' => [
+ [],
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ '{"status":200,"statustext":"OK","html":"<pre class=\"api-pretty-content\">Format MOCK: &lt;b>ok&lt;/b></pre>","modules":["mediawiki.apipretty"],"continue":null,"time":1234}',
+ [ 'wrappedhtml' => 1 ],
+ [ 'name' => 'mockfm' ]
+ ],
+ 'normal, with set status' => [
+ [],
+ "Format MOCK: <b>ok</b>",
+ [],
+ [ 'name' => 'mock', 'status' => 400 ]
+ ],
+ 'HTML format, with set status' => [
+ [],
+ '<pre class="api-pretty-content">Format MOCK: &lt;b>ok&lt;/b></pre>',
+ [],
+ [ 'name' => 'mockfm', 'status' => 400 ]
+ ],
+ 'wrapped HTML format, with set status' => [
+ [],
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ '{"status":400,"statustext":"Bad Request","html":"<pre class=\"api-pretty-content\">Format MOCK: &lt;b>ok&lt;/b></pre>","modules":["mediawiki.apipretty"],"continue":null,"time":1234}',
+ [ 'wrappedhtml' => 1 ],
+ [ 'name' => 'mockfm', 'status' => 400 ]
+ ],
+ 'wrapped HTML format, cross-domain-policy' => [
+ [ 'continue' => '< CrOsS-DoMaIn-PoLiCy >' ],
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ '{"status":200,"statustext":"OK","html":"<pre class=\"api-pretty-content\">Format MOCK: &lt;b>ok&lt;/b></pre>","modules":["mediawiki.apipretty"],"continue":"\u003C CrOsS-DoMaIn-PoLiCy \u003E","time":1234}',
+ [ 'wrappedhtml' => 1 ],
+ [ 'name' => 'mockfm' ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideFilenameEncoding
+ */
+ public function testFilenameEncoding( $filename, $expect ) {
+ $ret = parent::encodeData( [], [], [
+ 'name' => 'mock',
+ 'class' => ApiFormatBase::class,
+ 'factory' => function ( ApiMain $main, $format ) use ( $filename ) {
+ $mock = $this->getMockFormatter( $main, $format, [ 'getFilename' ] );
+ $mock->method( 'getFilename' )->willReturn( $filename );
+ return $mock;
+ },
+ 'returnPrinter' => true,
+ ] );
+ $response = $ret['printer']->getMain()->getRequest()->response();
+
+ $this->assertSame( "inline; $expect", $response->getHeader( 'Content-Disposition' ) );
+ }
+
+ public static function provideFilenameEncoding() {
+ return [
+ 'something simple' => [
+ 'foo.xyz', 'filename=foo.xyz'
+ ],
+ 'more complicated, but still simple' => [
+ 'foo.!#$%&\'*+-^_`|~', 'filename=foo.!#$%&\'*+-^_`|~'
+ ],
+ 'Needs quoting' => [
+ 'foo\\bar.xyz', 'filename="foo\\\\bar.xyz"'
+ ],
+ 'Needs quoting (2)' => [
+ 'foo (bar).xyz', 'filename="foo (bar).xyz"'
+ ],
+ 'Needs quoting (3)' => [
+ "foo\t\"b\x5car\"\0.xyz", "filename=\"foo\x5c\t\x5c\"b\x5c\x5car\x5c\"\x5c\0.xyz\""
+ ],
+ 'Non-ASCII characters' => [
+ 'f贸o b谩r.馃檶!',
+ "filename=\"f\xF3o b\xE1r.?!\"; filename*=UTF-8''f%C3%B3o%20b%C3%A1r.%F0%9F%99%8C!"
+ ]
+ ];
+ }
+
+ public function testBasics() {
+ $printer = $this->getMockFormatter( null, 'mock' );
+ $this->assertTrue( $printer->canPrintErrors() );
+ $this->assertSame(
+ 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats',
+ $printer->getHelpUrls()
+ );
+ }
+
+ public function testDisable() {
+ $this->setMwGlobals( [
+ 'wgApiFrameOptions' => 'DENY',
+ ] );
+
+ $printer = $this->getMockFormatter( null, 'mock' );
+ $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) {
+ $printer->printText( 'Foo' );
+ } );
+ $this->assertFalse( $printer->isDisabled() );
+ $printer->disable();
+ $this->assertTrue( $printer->isDisabled() );
+
+ $printer->setHttpStatus( 400 );
+ $printer->initPrinter();
+ $printer->execute();
+ ob_start();
+ $printer->closePrinter();
+ $this->assertSame( '', ob_get_clean() );
+ $response = $printer->getMain()->getRequest()->response();
+ $this->assertNull( $response->getHeader( 'Content-Type' ) );
+ $this->assertNull( $response->getHeader( 'X-Frame-Options' ) );
+ $this->assertNull( $response->getHeader( 'Content-Disposition' ) );
+ $this->assertNull( $response->getStatusCode() );
+ }
+
+ public function testNullMimeType() {
+ $this->setMwGlobals( [
+ 'wgApiFrameOptions' => 'DENY',
+ ] );
+
+ $printer = $this->getMockFormatter( null, 'mock', [ 'getMimeType' ] );
+ $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) {
+ $printer->printText( 'Foo' );
+ } );
+ $printer->method( 'getMimeType' )->willReturn( null );
+ $this->assertNull( $printer->getMimeType(), 'sanity check' );
+
+ $printer->initPrinter();
+ $printer->execute();
+ ob_start();
+ $printer->closePrinter();
+ $this->assertSame( 'Foo', ob_get_clean() );
+ $response = $printer->getMain()->getRequest()->response();
+ $this->assertNull( $response->getHeader( 'Content-Type' ) );
+ $this->assertNull( $response->getHeader( 'X-Frame-Options' ) );
+ $this->assertNull( $response->getHeader( 'Content-Disposition' ) );
+
+ $printer = $this->getMockFormatter( null, 'mockfm', [ 'getMimeType' ] );
+ $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) {
+ $printer->printText( 'Foo' );
+ } );
+ $printer->method( 'getMimeType' )->willReturn( null );
+ $this->assertNull( $printer->getMimeType(), 'sanity check' );
+ $this->assertTrue( $printer->getIsHtml(), 'sanity check' );
+
+ $printer->initPrinter();
+ $printer->execute();
+ ob_start();
+ $printer->closePrinter();
+ $this->assertSame( 'Foo', ob_get_clean() );
+ $response = $printer->getMain()->getRequest()->response();
+ $this->assertSame(
+ 'text/html; charset=utf-8', strtolower( $response->getHeader( 'Content-Type' ) )
+ );
+ $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) );
+ $this->assertSame(
+ 'inline; filename=api-result.html', $response->getHeader( 'Content-Disposition' )
+ );
+ }
+
+ public function testApiFrameOptions() {
+ $this->setMwGlobals( [ 'wgApiFrameOptions' => 'DENY' ] );
+ $printer = $this->getMockFormatter( null, 'mock' );
+ $printer->initPrinter();
+ $this->assertSame(
+ 'DENY',
+ $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
+ );
+
+ $this->setMwGlobals( [ 'wgApiFrameOptions' => 'SAMEORIGIN' ] );
+ $printer = $this->getMockFormatter( null, 'mock' );
+ $printer->initPrinter();
+ $this->assertSame(
+ 'SAMEORIGIN',
+ $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
+ );
+
+ $this->setMwGlobals( [ 'wgApiFrameOptions' => false ] );
+ $printer = $this->getMockFormatter( null, 'mock' );
+ $printer->initPrinter();
+ $this->assertNull(
+ $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
+ );
+ }
+
+ public function testForceDefaultParams() {
+ $context = new RequestContext;
+ $context->setRequest( new FauxRequest( [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], true ) );
+ $main = new ApiMain( $context );
+ $allowedParams = [
+ 'foo' => [],
+ 'bar' => [ ApiBase::PARAM_DFLT => 'bar?' ],
+ 'baz' => 'baz!',
+ ];
+
+ $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] );
+ $printer->method( 'getAllowedParams' )->willReturn( $allowedParams );
+ $this->assertEquals(
+ [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ],
+ $printer->extractRequestParams(),
+ 'sanity check'
+ );
+
+ $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] );
+ $printer->method( 'getAllowedParams' )->willReturn( $allowedParams );
+ $printer->forceDefaultParams();
+ $this->assertEquals(
+ [ 'foo' => null, 'bar' => 'bar?', 'baz' => 'baz!' ],
+ $printer->extractRequestParams()
+ );
+ }
+
+ public function testGetAllowedParams() {
+ $printer = $this->getMockFormatter( null, 'mock' );
+ $this->assertSame( [], $printer->getAllowedParams() );
+
+ $printer = $this->getMockFormatter( null, 'mockfm' );
+ $this->assertSame( [
+ 'wrappedhtml' => [
+ ApiBase::PARAM_DFLT => false,
+ ApiBase::PARAM_HELP_MSG => 'apihelp-format-param-wrappedhtml',
+ ]
+ ], $printer->getAllowedParams() );
+ }
+
+ public function testGetExamplesMessages() {
+ $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mock' ) );
+ $this->assertSame( [
+ 'action=query&meta=siteinfo&siprop=namespaces&format=mock'
+ => [ 'apihelp-format-example-generic', 'MOCK' ]
+ ], $printer->getExamplesMessages() );
+
+ $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mockfm' ) );
+ $this->assertSame( [
+ 'action=query&meta=siteinfo&siprop=namespaces&format=mockfm'
+ => [ 'apihelp-format-example-generic', 'MOCK' ]
+ ], $printer->getExamplesMessages() );
+ }
+
+ /**
+ * @dataProvider provideHtmlHeader
+ */
+ public function testHtmlHeader( $post, $registerNonHtml, $expect ) {
+ $context = new RequestContext;
+ $request = new FauxRequest( [ 'a' => 1, 'b' => 2 ], $post );
+ $request->setRequestURL( 'http://example.org/wx/api.php' );
+ $context->setRequest( $request );
+ $context->setLanguage( 'qqx' );
+ $main = new ApiMain( $context );
+ $printer = $this->getMockFormatter( $main, 'mockfm' );
+ $mm = $printer->getMain()->getModuleManager();
+ $mm->addModule( 'mockfm', 'format', ApiFormatBase::class, function () {
+ return $mock;
+ } );
+ if ( $registerNonHtml ) {
+ $mm->addModule( 'mock', 'format', ApiFormatBase::class, function () {
+ return $mock;
+ } );
+ }
+
+ $printer->initPrinter();
+ $printer->execute();
+ ob_start();
+ $printer->closePrinter();
+ $text = ob_get_clean();
+ $this->assertContains( $expect, $text );
+ }
+
+ public static function provideHtmlHeader() {
+ return [
+ [ false, false, '(api-format-prettyprint-header-only-html: MOCK)' ],
+ [ true, false, '(api-format-prettyprint-header-only-html: MOCK)' ],
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ [ false, true, '(api-format-prettyprint-header-hyperlinked: MOCK, mock, <a rel="nofollow" class="external free" href="http://example.org/wx/api.php?a=1&amp;b=2&amp;format=mock">http://example.org/wx/api.php?a=1&amp;b=2&amp;format=mock</a>)' ],
+ [ true, true, '(api-format-prettyprint-header: MOCK, mock)' ],
+ ];
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatJsonTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatJsonTest.php
new file mode 100644
index 00000000..7eb2a35e
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatJsonTest.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * @group API
+ * @covers ApiFormatJson
+ */
+class ApiFormatJsonTest extends ApiFormatTestBase {
+
+ protected $printerName = 'json';
+
+ private static function addFormatVersion( $format, $arr ) {
+ foreach ( $arr as &$p ) {
+ if ( !isset( $p[2] ) ) {
+ $p[2] = [ 'formatversion' => $format ];
+ } else {
+ $p[2]['formatversion'] = $format;
+ }
+ }
+ return $arr;
+ }
+
+ public static function provideGeneralEncoding() {
+ return array_merge(
+ self::addFormatVersion( 1, [
+ // Basic types
+ [ [ null ], '[null]' ],
+ [ [ true ], '[""]' ],
+ [ [ false ], '[]' ],
+ [ [ true, ApiResult::META_BC_BOOLS => [ 0 ] ], '[true]' ],
+ [ [ false, ApiResult::META_BC_BOOLS => [ 0 ] ], '[false]' ],
+ [ [ 42 ], '[42]' ],
+ [ [ 42.5 ], '[42.5]' ],
+ [ [ 1e42 ], '[1.0e+42]' ],
+ [ [ 'foo' ], '["foo"]' ],
+ [ [ 'f贸o' ], '["f\u00f3o"]' ],
+ [ [ 'f贸o' ], '["f贸o"]', [ 'utf8' => 1 ] ],
+
+ // Arrays and objects
+ [ [ [] ], '[[]]' ],
+ [ [ [ 1 ] ], '[[1]]' ],
+ [ [ [ 'x' => 1 ] ], '[{"x":1}]' ],
+ [ [ [ 2 => 1 ] ], '[{"2":1}]' ],
+ [ [ (object)[] ], '[{}]' ],
+ [ [ [ 1, ApiResult::META_TYPE => 'assoc' ] ], '[{"0":1}]' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'array' ] ], '[[1]]' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'kvp' ] ], '[{"x":1}]' ],
+ [
+ [ [
+ 'x' => 1,
+ ApiResult::META_TYPE => 'BCkvp',
+ ApiResult::META_KVP_KEY_NAME => 'key'
+ ] ],
+ '[[{"key":"x","*":1}]]'
+ ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCarray' ] ], '[{"x":1}]' ],
+ [ [ [ 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ] ], '[["a","b"]]' ],
+
+ // Content
+ [ [ 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ '{"*":"foo"}' ],
+
+ // BC Subelements
+ [ [ 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => [ 'foo' ] ],
+ '{"foo":{"*":"foo"}}' ],
+
+ // Callbacks
+ [ [ 1 ], '/**/myCallback([1])', [ 'callback' => 'myCallback' ] ],
+
+ // Cross-domain mangling
+ [ [ '< Cross-Domain-Policy >' ], '["\u003C Cross-Domain-Policy >"]' ],
+ ] ),
+ self::addFormatVersion( 2, [
+ // Basic types
+ [ [ null ], '[null]' ],
+ [ [ true ], '[true]' ],
+ [ [ false ], '[false]' ],
+ [ [ true, ApiResult::META_BC_BOOLS => [ 0 ] ], '[true]' ],
+ [ [ false, ApiResult::META_BC_BOOLS => [ 0 ] ], '[false]' ],
+ [ [ 42 ], '[42]' ],
+ [ [ 42.5 ], '[42.5]' ],
+ [ [ 1e42 ], '[1.0e+42]' ],
+ [ [ 'foo' ], '["foo"]' ],
+ [ [ 'f贸o' ], '["f贸o"]' ],
+ [ [ 'f贸o' ], '["f\u00f3o"]', [ 'ascii' => 1 ] ],
+
+ // Arrays and objects
+ [ [ [] ], '[[]]' ],
+ [ [ [ 'x' => 1 ] ], '[{"x":1}]' ],
+ [ [ [ 2 => 1 ] ], '[{"2":1}]' ],
+ [ [ (object)[] ], '[{}]' ],
+ [ [ [ 1, ApiResult::META_TYPE => 'assoc' ] ], '[{"0":1}]' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'array' ] ], '[[1]]' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'kvp' ] ], '[{"x":1}]' ],
+ [
+ [ [
+ 'x' => 1,
+ ApiResult::META_TYPE => 'BCkvp',
+ ApiResult::META_KVP_KEY_NAME => 'key'
+ ] ],
+ '[{"x":1}]'
+ ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCarray' ] ], '[[1]]' ],
+ [
+ [ [
+ 'a',
+ 'b',
+ ApiResult::META_TYPE => 'BCassoc'
+ ] ],
+ '[{"0":"a","1":"b"}]'
+ ],
+
+ // Content
+ [ [ 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ '{"content":"foo"}' ],
+
+ // BC Subelements
+ [ [ 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => [ 'foo' ] ],
+ '{"foo":"foo"}' ],
+
+ // Callbacks
+ [ [ 1 ], '/**/myCallback([1])', [ 'callback' => 'myCallback' ] ],
+
+ // Cross-domain mangling
+ [ [ '< Cross-Domain-Policy >' ], '["\u003C Cross-Domain-Policy >"]' ],
+ ] )
+ );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatNoneTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatNoneTest.php
new file mode 100644
index 00000000..87e36703
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatNoneTest.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @group API
+ * @covers ApiFormatNone
+ */
+class ApiFormatNoneTest extends ApiFormatTestBase {
+
+ protected $printerName = 'none';
+
+ public static function provideGeneralEncoding() {
+ return [
+ // Basic types
+ [ [ null ], '' ],
+ [ [ true ], '' ],
+ [ [ false ], '' ],
+ [ [ 42 ], '' ],
+ [ [ 42.5 ], '' ],
+ [ [ 1e42 ], '' ],
+ [ [ 'foo' ], '' ],
+ [ [ 'f贸o' ], '' ],
+
+ // Arrays and objects
+ [ [ [] ], '' ],
+ [ [ [ 1 ] ], '' ],
+ [ [ [ 'x' => 1 ] ], '' ],
+ [ [ [ 2 => 1 ] ], '' ],
+ [ [ (object)[] ], '' ],
+ [ [ [ 1, ApiResult::META_TYPE => 'assoc' ] ], '' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'array' ] ], '' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'kvp' ] ], '' ],
+ [
+ [ [
+ 'x' => 1,
+ ApiResult::META_TYPE => 'BCkvp',
+ ApiResult::META_KVP_KEY_NAME => 'key'
+ ] ],
+ ''
+ ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCarray' ] ], '' ],
+ [ [ [ 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ] ], '' ],
+
+ // Content
+ [ [ '*' => 'foo' ], '' ],
+
+ // BC Subelements
+ [ [ 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => [ 'foo' ] ], '' ],
+ ];
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
new file mode 100644
index 00000000..66e620e8
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @group API
+ * @covers ApiFormatPhp
+ */
+class ApiFormatPhpTest extends ApiFormatTestBase {
+
+ protected $printerName = 'php';
+
+ private static function addFormatVersion( $format, $arr ) {
+ foreach ( $arr as &$p ) {
+ if ( !isset( $p[2] ) ) {
+ $p[2] = [ 'formatversion' => $format ];
+ } else {
+ $p[2]['formatversion'] = $format;
+ }
+ }
+ return $arr;
+ }
+
+ public static function provideGeneralEncoding() {
+ // phpcs:disable Generic.Files.LineLength
+ return array_merge(
+ self::addFormatVersion( 1, [
+ // Basic types
+ [ [ null ], 'a:1:{i:0;N;}' ],
+ [ [ true ], 'a:1:{i:0;s:0:"";}' ],
+ [ [ false ], 'a:0:{}' ],
+ [ [ true, ApiResult::META_BC_BOOLS => [ 0 ] ],
+ 'a:1:{i:0;b:1;}' ],
+ [ [ false, ApiResult::META_BC_BOOLS => [ 0 ] ],
+ 'a:1:{i:0;b:0;}' ],
+ [ [ 42 ], 'a:1:{i:0;i:42;}' ],
+ [ [ 42.5 ], 'a:1:{i:0;d:42.5;}' ],
+ [ [ 1e42 ], 'a:1:{i:0;d:1.0E+42;}' ],
+ [ [ 'foo' ], 'a:1:{i:0;s:3:"foo";}' ],
+ [ [ 'f贸o' ], 'a:1:{i:0;s:4:"f贸o";}' ],
+
+ // Arrays and objects
+ [ [ [] ], 'a:1:{i:0;a:0:{}}' ],
+ [ [ [ 1 ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'x' => 1 ] ], 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ],
+ [ [ [ 2 => 1 ] ], 'a:1:{i:0;a:1:{i:2;i:1;}}' ],
+ [ [ (object)[] ], 'a:1:{i:0;a:0:{}}' ],
+ [ [ [ 1, ApiResult::META_TYPE => 'assoc' ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'array' ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'kvp' ] ], 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ] ],
+ 'a:1:{i:0;a:1:{i:0;a:2:{s:3:"key";s:1:"x";s:1:"*";i:1;}}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCarray' ] ], 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ],
+ [ [ [ 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ] ], 'a:1:{i:0;a:2:{i:0;s:1:"a";i:1;s:1:"b";}}' ],
+
+ // Content
+ [ [ 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ 'a:1:{s:1:"*";s:3:"foo";}' ],
+
+ // BC Subelements
+ [ [ 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => [ 'foo' ] ],
+ 'a:1:{s:3:"foo";a:1:{s:1:"*";s:3:"foo";}}' ],
+ ] ),
+ self::addFormatVersion( 2, [
+ // Basic types
+ [ [ null ], 'a:1:{i:0;N;}' ],
+ [ [ true ], 'a:1:{i:0;b:1;}' ],
+ [ [ false ], 'a:1:{i:0;b:0;}' ],
+ [ [ true, ApiResult::META_BC_BOOLS => [ 0 ] ],
+ 'a:1:{i:0;b:1;}' ],
+ [ [ false, ApiResult::META_BC_BOOLS => [ 0 ] ],
+ 'a:1:{i:0;b:0;}' ],
+ [ [ 42 ], 'a:1:{i:0;i:42;}' ],
+ [ [ 42.5 ], 'a:1:{i:0;d:42.5;}' ],
+ [ [ 1e42 ], 'a:1:{i:0;d:1.0E+42;}' ],
+ [ [ 'foo' ], 'a:1:{i:0;s:3:"foo";}' ],
+ [ [ 'f贸o' ], 'a:1:{i:0;s:4:"f贸o";}' ],
+
+ // Arrays and objects
+ [ [ [] ], 'a:1:{i:0;a:0:{}}' ],
+ [ [ [ 1 ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'x' => 1 ] ], 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ],
+ [ [ [ 2 => 1 ] ], 'a:1:{i:0;a:1:{i:2;i:1;}}' ],
+ [ [ (object)[] ], 'a:1:{i:0;a:0:{}}' ],
+ [ [ [ 1, ApiResult::META_TYPE => 'assoc' ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'array' ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'kvp' ] ], 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ] ],
+ 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCarray' ] ], 'a:1:{i:0;a:1:{i:0;i:1;}}' ],
+ [ [ [ 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ] ], 'a:1:{i:0;a:2:{i:0;s:1:"a";i:1;s:1:"b";}}' ],
+
+ // Content
+ [ [ 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ 'a:1:{s:7:"content";s:3:"foo";}' ],
+
+ // BC Subelements
+ [ [ 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => [ 'foo' ] ],
+ 'a:1:{s:3:"foo";s:3:"foo";}' ],
+ ] )
+ );
+ // phpcs:enable
+ }
+
+ public function testCrossDomainMangling() {
+ $config = new HashConfig( [ 'MangleFlashPolicy' => false ] );
+ $context = new RequestContext;
+ $context->setConfig( new MultiConfig( [
+ $config,
+ $context->getConfig(),
+ ] ) );
+ $main = new ApiMain( $context );
+ $main->getResult()->addValue( null, null, '< Cross-Domain-Policy >' );
+
+ $printer = $main->createPrinterByName( 'php' );
+ ob_start( 'MediaWiki\\OutputHandler::handle' );
+ $printer->initPrinter();
+ $printer->execute();
+ $printer->closePrinter();
+ $ret = ob_get_clean();
+ $this->assertSame( 'a:1:{i:0;s:23:"< Cross-Domain-Policy >";}', $ret );
+
+ $config->set( 'MangleFlashPolicy', true );
+ $printer = $main->createPrinterByName( 'php' );
+ ob_start( 'MediaWiki\\OutputHandler::handle' );
+ try {
+ $printer->initPrinter();
+ $printer->execute();
+ $printer->closePrinter();
+ ob_end_clean();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( ApiUsageException $ex ) {
+ ob_end_clean();
+ $this->assertTrue(
+ $ex->getStatusValue()->hasMessage( 'apierror-formatphp' ),
+ 'Expected exception'
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php
new file mode 100644
index 00000000..f64af6d3
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @group API
+ * @covers ApiFormatRaw
+ */
+class ApiFormatRawTest extends ApiFormatTestBase {
+
+ protected $printerName = 'raw';
+
+ /**
+ * Test basic encoding and missing mime and text exceptions
+ * @return array datasets
+ */
+ public static function provideGeneralEncoding() {
+ $options = [
+ 'class' => ApiFormatRaw::class,
+ 'factory' => function ( ApiMain $main ) {
+ return new ApiFormatRaw( $main, new ApiFormatJson( $main, 'json' ) );
+ }
+ ];
+
+ return [
+ [
+ [ 'mime' => 'text/plain', 'text' => 'foo' ],
+ 'foo',
+ [],
+ $options
+ ],
+ [
+ [ 'mime' => 'text/plain', 'text' => 'f贸o' ],
+ 'f贸o',
+ [],
+ $options
+ ],
+ [
+ [ 'text' => 'some text' ],
+ new MWException( 'No MIME type set for raw formatter' ),
+ [],
+ $options
+ ],
+ [
+ [ 'mime' => 'text/plain' ],
+ new MWException( 'No text given for raw formatter' ),
+ [],
+ $options
+ ],
+ 'test error fallback' => [
+ [ 'mime' => 'text/plain', 'text' => 'some text', 'error' => 'some error' ],
+ '{"mime":"text/plain","text":"some text","error":"some error"}',
+ [],
+ $options
+ ]
+ ];
+ }
+
+ /**
+ * Test specifying filename
+ */
+ public function testFilename() {
+ $printer = new ApiFormatRaw( new ApiMain );
+ $printer->getResult()->addValue( null, 'filename', 'whatever.raw' );
+ $this->assertSame( 'whatever.raw', $printer->getFilename() );
+ }
+
+ /**
+ * Test specifying filename with error fallback printer
+ */
+ public function testErrorFallbackFilename() {
+ $apiMain = new ApiMain;
+ $printer = new ApiFormatRaw( $apiMain, new ApiFormatJson( $apiMain, 'json' ) );
+ $printer->getResult()->addValue( null, 'error', 'some error' );
+ $printer->getResult()->addValue( null, 'filename', 'whatever.raw' );
+ $this->assertSame( 'api-result.json', $printer->getFilename() );
+ }
+
+ /**
+ * Test specifying mime
+ */
+ public function testMime() {
+ $printer = new ApiFormatRaw( new ApiMain );
+ $printer->getResult()->addValue( null, 'mime', 'text/plain' );
+ $this->assertSame( 'text/plain', $printer->getMimeType() );
+ }
+
+ /**
+ * Test specifying mime with error fallback printer
+ */
+ public function testErrorFallbackMime() {
+ $apiMain = new ApiMain;
+ $printer = new ApiFormatRaw( $apiMain, new ApiFormatJson( $apiMain, 'json' ) );
+ $printer->getResult()->addValue( null, 'error', 'some error' );
+ $printer->getResult()->addValue( null, 'mime', 'text/plain' );
+ $this->assertSame( 'application/json', $printer->getMimeType() );
+ }
+
+ /**
+ * Check that setting failWithHTTPError to true will result in 400 response status code
+ */
+ public function testFailWithHTTPError() {
+ $apiMain = null;
+
+ $this->testGeneralEncoding(
+ [ 'mime' => 'text/plain', 'text' => 'some text', 'error' => 'some error' ],
+ '{"mime":"text/plain","text":"some text","error":"some error"}',
+ [],
+ [
+ 'class' => ApiFormatRaw::class,
+ 'factory' => function ( ApiMain $main ) use ( &$apiMain ) {
+ $apiMain = $main;
+ $printer = new ApiFormatRaw( $main, new ApiFormatJson( $main, 'json' ) );
+ $printer->setFailWithHTTPError( true );
+ return $printer;
+ }
+ ]
+ );
+ $this->assertEquals( 400, $apiMain->getRequest()->response()->getStatusCode() );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php
new file mode 100644
index 00000000..4169dab2
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php
@@ -0,0 +1,93 @@
+<?php
+
+abstract class ApiFormatTestBase extends MediaWikiTestCase {
+
+ /**
+ * Name of the formatter being tested
+ * @var string
+ */
+ protected $printerName;
+
+ /**
+ * Return general data to be encoded for testing
+ * @return array See self::testGeneralEncoding
+ * @throws BadMethodCallException
+ */
+ public static function provideGeneralEncoding() {
+ throw new BadMethodCallException( static::class . ' must implement ' . __METHOD__ );
+ }
+
+ /**
+ * Get the formatter output for the given input data
+ * @param array $params Query parameters
+ * @param array $data Data to encode
+ * @param array $options Options. If passed a string, the string is treated
+ * as the 'class' option.
+ * - name: Format name, rather than $this->printerName
+ * - class: If set, register 'name' with this class (and 'factory', if that's set)
+ * - factory: Used with 'class' to register at runtime
+ * - returnPrinter: Return the printer object
+ * @param callable|null $factory Factory to use instead of the normal one
+ * @return string|array The string if $options['returnPrinter'] isn't set, or an array if it is:
+ * - text: Output text string
+ * - printer: ApiFormatBase
+ * @throws Exception
+ */
+ protected function encodeData( array $params, array $data, $options = [] ) {
+ if ( is_string( $options ) ) {
+ $options = [ 'class' => $options ];
+ }
+ $printerName = isset( $options['name'] ) ? $options['name'] : $this->printerName;
+
+ $context = new RequestContext;
+ $context->setRequest( new FauxRequest( $params, true ) );
+ $main = new ApiMain( $context );
+ if ( isset( $options['class'] ) ) {
+ $factory = isset( $options['factory'] ) ? $options['factory'] : null;
+ $main->getModuleManager()->addModule( $printerName, 'format', $options['class'], $factory );
+ }
+ $result = $main->getResult();
+ $result->addArrayType( null, 'default' );
+ foreach ( $data as $k => $v ) {
+ $result->addValue( null, $k, $v );
+ }
+
+ $ret = [];
+ $printer = $main->createPrinterByName( $printerName );
+ $printer->initPrinter();
+ $printer->execute();
+ ob_start();
+ try {
+ $printer->closePrinter();
+ $ret['text'] = ob_get_clean();
+ } catch ( Exception $ex ) {
+ ob_end_clean();
+ throw $ex;
+ }
+
+ if ( !empty( $options['returnPrinter'] ) ) {
+ $ret['printer'] = $printer;
+ }
+
+ return count( $ret ) === 1 ? $ret['text'] : $ret;
+ }
+
+ /**
+ * @dataProvider provideGeneralEncoding
+ * @param array $data Data to be encoded
+ * @param string|Exception $expect String to expect, or exception expected to be thrown
+ * @param array $params Query parameters to set in the FauxRequest
+ * @param array $options Options to pass to self::encodeData()
+ */
+ public function testGeneralEncoding(
+ array $data, $expect, array $params = [], array $options = []
+ ) {
+ if ( $expect instanceof Exception ) {
+ $this->setExpectedException( get_class( $expect ), $expect->getMessage() );
+ $this->encodeData( $params, $data, $options ); // Should throw
+ } else {
+ $this->assertSame( $expect, $this->encodeData( $params, $data, $options ) );
+ }
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php
new file mode 100644
index 00000000..915fb5c5
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @covers ApiFormatXml
+ */
+class ApiFormatXmlTest extends ApiFormatTestBase {
+
+ protected $printerName = 'xml';
+
+ public static function setUpBeforeClass() {
+ parent::setUpBeforeClass();
+ $page = WikiPage::factory( Title::newFromText( 'MediaWiki:ApiFormatXmlTest.xsl' ) );
+ // phpcs:disable Generic.Files.LineLength
+ $page->doEditContent( new WikitextContent(
+ '<?xml version="1.0"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" />'
+ ), 'Summary' );
+ // phpcs:enable
+ $page = WikiPage::factory( Title::newFromText( 'MediaWiki:ApiFormatXmlTest' ) );
+ $page->doEditContent( new WikitextContent( 'Bogus' ), 'Summary' );
+ $page = WikiPage::factory( Title::newFromText( 'ApiFormatXmlTest' ) );
+ $page->doEditContent( new WikitextContent( 'Bogus' ), 'Summary' );
+ }
+
+ public static function provideGeneralEncoding() {
+ // phpcs:disable Generic.Files.LineLength
+ return [
+ // Basic types
+ [ [ null, 'a' => null ], '<?xml version="1.0"?><api><_v _idx="0" /></api>' ],
+ [ [ true, 'a' => true ], '<?xml version="1.0"?><api a=""><_v _idx="0">true</_v></api>' ],
+ [ [ false, 'a' => false ], '<?xml version="1.0"?><api><_v _idx="0">false</_v></api>' ],
+ [ [ true, 'a' => true, ApiResult::META_BC_BOOLS => [ 0, 'a' ] ],
+ '<?xml version="1.0"?><api a=""><_v _idx="0">1</_v></api>' ],
+ [ [ false, 'a' => false, ApiResult::META_BC_BOOLS => [ 0, 'a' ] ],
+ '<?xml version="1.0"?><api><_v _idx="0"></_v></api>' ],
+ [ [ 42, 'a' => 42 ], '<?xml version="1.0"?><api a="42"><_v _idx="0">42</_v></api>' ],
+ [ [ 42.5, 'a' => 42.5 ], '<?xml version="1.0"?><api a="42.5"><_v _idx="0">42.5</_v></api>' ],
+ [ [ 1e42, 'a' => 1e42 ], '<?xml version="1.0"?><api a="1.0E+42"><_v _idx="0">1.0E+42</_v></api>' ],
+ [ [ 'foo', 'a' => 'foo' ], '<?xml version="1.0"?><api a="foo"><_v _idx="0">foo</_v></api>' ],
+ [ [ 'f贸o', 'a' => 'f贸o' ], '<?xml version="1.0"?><api a="f贸o"><_v _idx="0">f贸o</_v></api>' ],
+
+ // Arrays and objects
+ [ [ [] ], '<?xml version="1.0"?><api><_v /></api>' ],
+ [ [ [ 'x' => 1 ] ], '<?xml version="1.0"?><api><_v x="1" /></api>' ],
+ [ [ [ 2 => 1 ] ], '<?xml version="1.0"?><api><_v><_v _idx="2">1</_v></_v></api>' ],
+ [ [ (object)[] ], '<?xml version="1.0"?><api><_v /></api>' ],
+ [ [ [ 1, ApiResult::META_TYPE => 'assoc' ] ], '<?xml version="1.0"?><api><_v><_v _idx="0">1</_v></_v></api>' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'array' ] ], '<?xml version="1.0"?><api><_v><_v>1</_v></_v></api>' ],
+ [ [ [ 'x' => 1, 'y' => [ 'z' => 1 ], ApiResult::META_TYPE => 'kvp' ] ],
+ '<?xml version="1.0"?><api><_v><_v _name="x" xml:space="preserve">1</_v><_v _name="y"><z xml:space="preserve">1</z></_v></_v></api>' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'kvp', ApiResult::META_INDEXED_TAG_NAME => 'i', ApiResult::META_KVP_KEY_NAME => 'key' ] ],
+ '<?xml version="1.0"?><api><_v><i key="x" xml:space="preserve">1</i></_v></api>' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ] ],
+ '<?xml version="1.0"?><api><_v><_v key="x" xml:space="preserve">1</_v></_v></api>' ],
+ [ [ [ 'x' => 1, ApiResult::META_TYPE => 'BCarray' ] ], '<?xml version="1.0"?><api><_v x="1" /></api>' ],
+ [ [ [ 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ] ], '<?xml version="1.0"?><api><_v><_v _idx="0">a</_v><_v _idx="1">b</_v></_v></api>' ],
+
+ // Content
+ [ [ 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ '<?xml version="1.0"?><api xml:space="preserve">foo</api>' ],
+
+ // Specified element name
+ [ [ 'foo', 'bar', ApiResult::META_INDEXED_TAG_NAME => 'itn' ],
+ '<?xml version="1.0"?><api><itn>foo</itn><itn>bar</itn></api>' ],
+
+ // Subelements
+ [ [ 'a' => 1, 's' => 1, '_subelements' => [ 's' ] ],
+ '<?xml version="1.0"?><api a="1"><s xml:space="preserve">1</s></api>' ],
+
+ // Content and subelement
+ [ [ 'a' => 1, 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ '<?xml version="1.0"?><api a="1" xml:space="preserve">foo</api>' ],
+ [ [ 's' => [], 'content' => 'foo', ApiResult::META_CONTENT => 'content' ],
+ '<?xml version="1.0"?><api><s /><content xml:space="preserve">foo</content></api>' ],
+ [
+ [
+ 's' => 1,
+ 'content' => 'foo',
+ ApiResult::META_CONTENT => 'content',
+ ApiResult::META_SUBELEMENTS => [ 's' ]
+ ],
+ '<?xml version="1.0"?><api><s xml:space="preserve">1</s><content xml:space="preserve">foo</content></api>'
+ ],
+
+ // BC Subelements
+ [ [ 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => [ 'foo' ] ],
+ '<?xml version="1.0"?><api><foo xml:space="preserve">foo</foo></api>' ],
+
+ // Name mangling
+ [ [ 'foo.bar' => 1 ], '<?xml version="1.0"?><api foo.bar="1" />' ],
+ [ [ '' => 1 ], '<?xml version="1.0"?><api _="1" />' ],
+ [ [ 'foo bar' => 1 ], '<?xml version="1.0"?><api _foo.20.bar="1" />' ],
+ [ [ 'foo:bar' => 1 ], '<?xml version="1.0"?><api _foo.3A.bar="1" />' ],
+ [ [ 'foo%.bar' => 1 ], '<?xml version="1.0"?><api _foo.25..2E.bar="1" />' ],
+ [ [ '4foo' => 1, 'foo4' => 1 ], '<?xml version="1.0"?><api _4foo="1" foo4="1" />' ],
+ [ [ "foo\xe3\x80\x80bar" => 1 ], '<?xml version="1.0"?><api _foo.3000.bar="1" />' ],
+ [ [ 'foo:bar' => 1, ApiResult::META_PRESERVE_KEYS => [ 'foo:bar' ] ],
+ '<?xml version="1.0"?><api foo:bar="1" />' ],
+ [ [ 'a', 'b', ApiResult::META_INDEXED_TAG_NAME => 'foo bar' ],
+ '<?xml version="1.0"?><api><_foo.20.bar>a</_foo.20.bar><_foo.20.bar>b</_foo.20.bar></api>' ],
+
+ // includenamespace param
+ [ [ 'x' => 'foo' ], '<?xml version="1.0"?><api x="foo" xmlns="http://www.mediawiki.org/xml/api/" />',
+ [ 'includexmlnamespace' => 1 ] ],
+
+ // xslt param
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified.</xml></warnings></api>',
+ [ 'xslt' => 'DoesNotExist' ] ],
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should be in the MediaWiki namespace.</xml></warnings></api>',
+ [ 'xslt' => 'ApiFormatXmlTest' ] ],
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have &quot;.xsl&quot; extension.</xml></warnings></api>',
+ [ 'xslt' => 'MediaWiki:ApiFormatXmlTest' ] ],
+ [ [],
+ '<?xml version="1.0"?><?xml-stylesheet href="' .
+ htmlspecialchars( Title::newFromText( 'MediaWiki:ApiFormatXmlTest.xsl' )->getLocalURL( 'action=raw' ) ) .
+ '" type="text/xsl" ?><api />',
+ [ 'xslt' => 'MediaWiki:ApiFormatXmlTest.xsl' ] ],
+ ];
+ // phpcs:enable
+ }
+
+}