3 namespace MediaWiki\Tests\Api\Format
;
5 use MediaWiki\Api\ApiBase
;
6 use MediaWiki\Api\ApiFormatBase
;
7 use MediaWiki\Api\ApiMain
;
8 use MediaWiki\Context\RequestContext
;
9 use MediaWiki\MainConfigNames
;
10 use MediaWiki\Request\FauxRequest
;
11 use PHPUnit\Framework\MockObject\MockObject
;
12 use Wikimedia\ParamValidator\ParamValidator
;
13 use Wikimedia\TestingAccessWrapper
;
18 * @covers \MediaWiki\Api\ApiFormatBase
20 class ApiFormatBaseTest
extends ApiFormatTestBase
{
23 protected $printerName = 'mockbase';
25 protected function setUp(): void
{
27 $this->overrideConfigValue( MainConfigNames
::Server
, 'http://example.org' );
31 * @param ApiMain|null $main
32 * @param string $format
33 * @param array $methods
34 * @return ApiFormatBase|MockObject
36 public function getMockFormatter( ?ApiMain
$main, $format, $methods = [] ) {
37 if ( $main === null ) {
38 $context = new RequestContext
;
39 $context->setRequest( new FauxRequest( [], true ) );
40 $main = new ApiMain( $context );
43 $mock = $this->getMockBuilder( ApiFormatBase
::class )
44 ->setConstructorArgs( [ $main, $format ] )
45 ->onlyMethods( array_unique( array_merge( $methods, [ 'getMimeType', 'execute' ] ) ) )
47 if ( !in_array( 'getMimeType', $methods, true ) ) {
48 $mock->method( 'getMimeType' )->willReturn( 'text/x-mock' );
53 protected function encodeData( array $params, array $data, $options = [] ) {
56 'class' => ApiFormatBase
::class,
57 'factory' => function ( ApiMain
$main, $format ) use ( $options ) {
58 $mock = $this->getMockFormatter( $main, $format );
59 $mock->expects( $this->once() )->method( 'execute' )
60 ->willReturnCallback( static function () use ( $mock ) {
61 $mock->printText( "Format {$mock->getFormat()}: " );
62 $mock->printText( "<b>ok</b>" );
65 if ( isset( $options['status'] ) ) {
66 $mock->setHttpStatus( $options['status'] );
71 'returnPrinter' => true,
74 $this->overrideConfigValue( MainConfigNames
::ApiFrameOptions
, 'DENY' );
76 $ret = parent
::encodeData( $params, $data, $options );
77 /** @var ApiFormatBase $printer */
78 $printer = $ret['printer'];
81 if ( $options['name'] !== 'mockfm' ) {
83 $file = 'api-result.mock';
84 $status = $options['status'] ??
null;
85 } elseif ( isset( $params['wrappedhtml'] ) ) {
86 $ct = 'text/mediawiki-api-prettyprint-wrapped';
87 $file = 'api-result-wrapped.json';
90 // Replace varying field
91 $text = preg_replace( '/"time":\d+/', '"time":1234', $text );
94 $file = 'api-result.html';
97 // Strip OutputPage-generated HTML
98 if ( preg_match( '!<pre class="api-pretty-content">.*</pre>!s', $text, $m ) ) {
103 $response = $printer->getMain()->getRequest()->response();
104 $this->assertSame( "$ct; charset=utf-8", strtolower( $response->getHeader( 'Content-Type' ) ) );
105 $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) );
106 $this->assertSame( $file, $printer->getFilename() );
107 $this->assertSame( "inline; filename=$file", $response->getHeader( 'Content-Disposition' ) );
108 $this->assertSame( $status, $response->getStatusCode() );
113 public static function provideGeneralEncoding() {
117 "Format MOCK: <b>ok</b>",
121 'normal ignores wrappedhtml' => [
123 "Format MOCK: <b>ok</b>",
124 [ 'wrappedhtml' => 1 ],
129 '<pre class="api-pretty-content">Format MOCK: <b>ok</b></pre>',
131 [ 'name' => 'mockfm' ]
133 'wrapped HTML format' => [
135 '{"status":200,"statustext":"OK","html":"<pre class=\"api-pretty-content\">Format MOCK: <b>ok</b></pre>","modules":["mediawiki.apipretty"],"continue":null,"time":1234}',
136 [ 'wrappedhtml' => 1 ],
137 [ 'name' => 'mockfm' ]
139 'normal, with set status' => [
141 "Format MOCK: <b>ok</b>",
143 [ 'name' => 'mock', 'status' => 400 ]
145 'HTML format, with set status' => [
147 '<pre class="api-pretty-content">Format MOCK: <b>ok</b></pre>',
149 [ 'name' => 'mockfm', 'status' => 400 ]
151 'wrapped HTML format, with set status' => [
153 '{"status":400,"statustext":"Bad Request","html":"<pre class=\"api-pretty-content\">Format MOCK: <b>ok</b></pre>","modules":["mediawiki.apipretty"],"continue":null,"time":1234}',
154 [ 'wrappedhtml' => 1 ],
155 [ 'name' => 'mockfm', 'status' => 400 ]
161 * @dataProvider provideFilenameEncoding
163 public function testFilenameEncoding( $filename, $expect ) {
164 $ret = parent
::encodeData( [], [], [
166 'class' => ApiFormatBase
::class,
167 'factory' => function ( ApiMain
$main, $format ) use ( $filename ) {
168 $mock = $this->getMockFormatter( $main, $format, [ 'getFilename' ] );
169 $mock->method( 'getFilename' )->willReturn( $filename );
172 'returnPrinter' => true,
174 $response = $ret['printer']->getMain()->getRequest()->response();
176 $this->assertSame( "inline; $expect", $response->getHeader( 'Content-Disposition' ) );
179 public static function provideFilenameEncoding() {
181 'something simple' => [
182 'foo.xyz', 'filename=foo.xyz'
184 'more complicated, but still simple' => [
185 'foo.!#$%&\'*+-^_`|~', 'filename=foo.!#$%&\'*+-^_`|~'
188 'foo\\bar.xyz', 'filename="foo\\\\bar.xyz"'
190 'Needs quoting (2)' => [
191 'foo (bar).xyz', 'filename="foo (bar).xyz"'
193 'Needs quoting (3)' => [
194 "foo\t\"b\x5car\"\0.xyz", "filename=\"foo\x5c\t\x5c\"b\x5c\x5car\x5c\"\x5c\0.xyz\""
196 'Non-ASCII characters' => [
198 "filename=\"f\xF3o b\xE1r.?!\"; filename*=UTF-8''f%C3%B3o%20b%C3%A1r.%F0%9F%99%8C!"
203 public function testBasics() {
204 $printer = $this->getMockFormatter( null, 'mock' );
205 $this->assertTrue( $printer->canPrintErrors() );
207 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats',
208 $printer->getHelpUrls()
212 public function testDisable() {
213 $this->overrideConfigValue( MainConfigNames
::ApiFrameOptions
, 'DENY' );
215 $printer = $this->getMockFormatter( null, 'mock' );
216 $printer->method( 'execute' )->willReturnCallback( static function () use ( $printer ) {
217 $printer->printText( 'Foo' );
219 $this->assertFalse( $printer->isDisabled() );
221 $this->assertTrue( $printer->isDisabled() );
223 $printer->setHttpStatus( 400 );
224 $printer->initPrinter();
227 $printer->closePrinter();
228 $this->assertSame( '', ob_get_clean() );
229 $response = $printer->getMain()->getRequest()->response();
230 $this->assertNull( $response->getHeader( 'Content-Type' ) );
231 $this->assertNull( $response->getHeader( 'X-Frame-Options' ) );
232 $this->assertNull( $response->getHeader( 'Content-Disposition' ) );
233 $this->assertNull( $response->getStatusCode() );
236 public function testNullMimeType() {
237 $this->overrideConfigValue( MainConfigNames
::ApiFrameOptions
, 'DENY' );
239 $printer = $this->getMockFormatter( null, 'mock', [ 'getMimeType' ] );
240 $printer->method( 'execute' )->willReturnCallback( static function () use ( $printer ) {
241 $printer->printText( 'Foo' );
243 $printer->method( 'getMimeType' )->willReturn( null );
244 $this->assertNull( $printer->getMimeType() );
246 $printer->initPrinter();
249 $printer->closePrinter();
250 $this->assertSame( 'Foo', ob_get_clean() );
251 $response = $printer->getMain()->getRequest()->response();
252 $this->assertNull( $response->getHeader( 'Content-Type' ) );
253 $this->assertNull( $response->getHeader( 'X-Frame-Options' ) );
254 $this->assertNull( $response->getHeader( 'Content-Disposition' ) );
256 $printer = $this->getMockFormatter( null, 'mockfm', [ 'getMimeType' ] );
257 $printer->method( 'execute' )->willReturnCallback( static function () use ( $printer ) {
258 $printer->printText( 'Foo' );
260 $printer->method( 'getMimeType' )->willReturn( null );
261 $this->assertNull( $printer->getMimeType() );
262 $this->assertTrue( $printer->getIsHtml() );
264 $printer->initPrinter();
267 $printer->closePrinter();
268 $this->assertSame( 'Foo', ob_get_clean() );
269 $response = $printer->getMain()->getRequest()->response();
271 'text/html; charset=utf-8', strtolower( $response->getHeader( 'Content-Type' ) )
273 $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) );
275 'inline; filename=api-result.html', $response->getHeader( 'Content-Disposition' )
279 public static function provideApiFrameOptions() {
280 yield
'Override ApiFrameOptions to DENY' => [ 'DENY', 'DENY' ];
281 yield
'Override ApiFrameOptions to SAMEORIGIN' => [ 'SAMEORIGIN', 'SAMEORIGIN' ];
282 yield
'Override ApiFrameOptions to false' => [ false, null ];
286 * @dataProvider provideApiFrameOptions
288 public function testApiFrameOptions( $customConfig, $expectedHeader ) {
289 $this->overrideConfigValue( MainConfigNames
::ApiFrameOptions
, $customConfig );
290 $printer = $this->getMockFormatter( null, 'mock' );
291 $printer->initPrinter();
294 $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
298 public function testForceDefaultParams() {
299 $context = new RequestContext
;
300 $context->setRequest( new FauxRequest( [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], true ) );
301 $main = new ApiMain( $context );
304 'bar' => [ ParamValidator
::PARAM_DEFAULT
=> 'bar?' ],
308 $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] );
309 $printer->method( 'getAllowedParams' )->willReturn( $allowedParams );
311 [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ],
312 $printer->extractRequestParams()
315 $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] );
316 $printer->method( 'getAllowedParams' )->willReturn( $allowedParams );
317 $printer->forceDefaultParams();
319 [ 'foo' => null, 'bar' => 'bar?', 'baz' => 'baz!' ],
320 $printer->extractRequestParams()
324 public function testGetAllowedParams() {
325 $printer = $this->getMockFormatter( null, 'mock' );
326 $this->assertSame( [], $printer->getAllowedParams() );
328 $printer = $this->getMockFormatter( null, 'mockfm' );
331 ParamValidator
::PARAM_DEFAULT
=> false,
332 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-format-param-wrappedhtml',
334 ], $printer->getAllowedParams() );
337 public function testGetExamplesMessages() {
338 /** @var ApiFormatBase $printer */
339 $printer = TestingAccessWrapper
::newFromObject( $this->getMockFormatter( null, 'mock' ) );
341 'action=query&meta=siteinfo&siprop=namespaces&format=mock'
342 => [ 'apihelp-format-example-generic', 'MOCK' ]
343 ], $printer->getExamplesMessages() );
345 $printer = TestingAccessWrapper
::newFromObject( $this->getMockFormatter( null, 'mockfm' ) );
347 'action=query&meta=siteinfo&siprop=namespaces&format=mockfm'
348 => [ 'apihelp-format-example-generic', 'MOCK' ]
349 ], $printer->getExamplesMessages() );
353 * @dataProvider provideHtmlHeader
355 public function testHtmlHeader( $post, $registerNonHtml, $expect ) {
356 $context = new RequestContext
;
357 $request = new FauxRequest( [ 'a' => 1, 'b' => 2 ], $post );
358 $request->setRequestURL( '/wx/api.php' );
359 $context->setRequest( $request );
360 $context->setLanguage( 'qqx' );
361 $main = new ApiMain( $context );
362 $printer = $this->getMockFormatter( $main, 'mockfm' );
363 $mm = $printer->getMain()->getModuleManager();
364 $mm->addModule( 'mockfm', 'format', [
365 'class' => ApiFormatBase
::class,
366 'factory' => static function () {
370 if ( $registerNonHtml ) {
371 $mm->addModule( 'mock', 'format', [
372 'class' => ApiFormatBase
::class,
373 'factory' => static function () {
379 $printer->initPrinter();
382 $printer->closePrinter();
383 $text = ob_get_clean();
384 $this->assertStringContainsString( $expect, $text );
385 $this->assertSame( 'private, must-revalidate, max-age=0', $main->getContext()->getRequest()->response()->getHeader( 'Cache-Control' ) );
388 public static function provideHtmlIsPrivate() {
389 yield
[ 'private', 'private' ];
390 yield
[ 'public', 'anon-public-user-private' ];
394 * Assert that HTML output is not cacheable (T354045).
395 * @dataProvider provideHtmlIsPrivate
397 public function testHtmlIsPrivate( $moduleCacheMode, $expectedCacheMode ) {
398 $context = new RequestContext
;
399 $request = new FauxRequest( [ 'uselang' => 'qqx' ] );
400 $request->setRequestURL( '/wx/api.php' );
401 $context->setRequest( $request );
402 $context->setLanguage( 'qqx' );
403 $main = new ApiMain( $context );
405 $printer = $this->getMockFormatter( $main, 'mockfm' );
406 $mm = $printer->getMain()->getModuleManager();
407 $mm->addModule( 'mockfm', 'format', [
408 'class' => ApiFormatBase
::class,
409 'factory' => static function () {
414 // pretend the output is cacheable
415 $main->setCacheMode( $moduleCacheMode );
416 $printer->initPrinter();
418 $mainAccess = TestingAccessWrapper
::newFromObject( $main );
419 $this->assertSame( $expectedCacheMode, $main->getCacheMode() );
421 $mainAccess->sendCacheHeaders( false );
423 'private, must-revalidate, max-age=0',
424 $request->response()->getHeader( 'cache-control' )
428 public static function provideHtmlHeader() {
430 [ false, false, '(api-format-prettyprint-header-only-html: MOCK)' ],
431 [ true, false, '(api-format-prettyprint-header-only-html: MOCK)' ],
432 [ 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>)' ],
433 [ true, true, '(api-format-prettyprint-header: MOCK, mock)' ],