diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/api/format')
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: <b>ok</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: <b>ok</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: <b>ok</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: <b>ok</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: <b>ok</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&b=2&format=mock">http://example.org/wx/api.php?a=1&b=2&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 ".xsl" 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 + } + +} |