Merge "Revert "Log the reason why revision->getContent() returns null""
[lhc/web/wiklou.git] / tests / phpunit / includes / api / format / ApiFormatBaseTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6 * @group API
7 * @covers ApiFormatBase
8 */
9 class ApiFormatBaseTest extends ApiFormatTestBase {
10
11 protected $printerName = 'mockbase';
12
13 public function getMockFormatter( ApiMain $main = null, $format, $methods = [] ) {
14 if ( $main === null ) {
15 $context = new RequestContext;
16 $context->setRequest( new FauxRequest( [], true ) );
17 $main = new ApiMain( $context );
18 }
19
20 $mock = $this->getMockBuilder( ApiFormatBase::class )
21 ->setConstructorArgs( [ $main, $format ] )
22 ->setMethods( array_unique( array_merge( $methods, [ 'getMimeType', 'execute' ] ) ) )
23 ->getMock();
24 if ( !in_array( 'getMimeType', $methods, true ) ) {
25 $mock->method( 'getMimeType' )->willReturn( 'text/x-mock' );
26 }
27 return $mock;
28 }
29
30 protected function encodeData( array $params, array $data, $options = [] ) {
31 $options += [
32 'name' => 'mock',
33 'class' => ApiFormatBase::class,
34 'factory' => function ( ApiMain $main, $format ) use ( $options ) {
35 $mock = $this->getMockFormatter( $main, $format );
36 $mock->expects( $this->once() )->method( 'execute' )
37 ->willReturnCallback( function () use ( $mock ) {
38 $mock->printText( "Format {$mock->getFormat()}: " );
39 $mock->printText( "<b>ok</b>" );
40 } );
41
42 if ( isset( $options['status'] ) ) {
43 $mock->setHttpStatus( $options['status'] );
44 }
45
46 return $mock;
47 },
48 'returnPrinter' => true,
49 ];
50
51 $this->setMwGlobals( [
52 'wgApiFrameOptions' => 'DENY',
53 ] );
54
55 $ret = parent::encodeData( $params, $data, $options );
56 $printer = TestingAccessWrapper::newFromObject( $ret['printer'] );
57 $text = $ret['text'];
58
59 if ( $options['name'] !== 'mockfm' ) {
60 $ct = 'text/x-mock';
61 $file = 'api-result.mock';
62 $status = isset( $options['status'] ) ? $options['status'] : null;
63 } elseif ( isset( $params['wrappedhtml'] ) ) {
64 $ct = 'text/mediawiki-api-prettyprint-wrapped';
65 $file = 'api-result-wrapped.json';
66 $status = null;
67
68 // Replace varying field
69 $text = preg_replace( '/"time":\d+/', '"time":1234', $text );
70 } else {
71 $ct = 'text/html';
72 $file = 'api-result.html';
73 $status = null;
74
75 // Strip OutputPage-generated HTML
76 if ( preg_match( '!<pre class="api-pretty-content">.*</pre>!s', $text, $m ) ) {
77 $text = $m[0];
78 }
79 }
80
81 $response = $printer->getMain()->getRequest()->response();
82 $this->assertSame( "$ct; charset=utf-8", strtolower( $response->getHeader( 'Content-Type' ) ) );
83 $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) );
84 $this->assertSame( $file, $printer->getFilename() );
85 $this->assertSame( "inline; filename=$file", $response->getHeader( 'Content-Disposition' ) );
86 $this->assertSame( $status, $response->getStatusCode() );
87
88 return $text;
89 }
90
91 public static function provideGeneralEncoding() {
92 return [
93 'normal' => [
94 [],
95 "Format MOCK: <b>ok</b>",
96 [],
97 [ 'name' => 'mock' ]
98 ],
99 'normal ignores wrappedhtml' => [
100 [],
101 "Format MOCK: <b>ok</b>",
102 [ 'wrappedhtml' => 1 ],
103 [ 'name' => 'mock' ]
104 ],
105 'HTML format' => [
106 [],
107 '<pre class="api-pretty-content">Format MOCK: &lt;b>ok&lt;/b></pre>',
108 [],
109 [ 'name' => 'mockfm' ]
110 ],
111 'wrapped HTML format' => [
112 [],
113 // phpcs:ignore Generic.Files.LineLength.TooLong
114 '{"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}',
115 [ 'wrappedhtml' => 1 ],
116 [ 'name' => 'mockfm' ]
117 ],
118 'normal, with set status' => [
119 [],
120 "Format MOCK: <b>ok</b>",
121 [],
122 [ 'name' => 'mock', 'status' => 400 ]
123 ],
124 'HTML format, with set status' => [
125 [],
126 '<pre class="api-pretty-content">Format MOCK: &lt;b>ok&lt;/b></pre>',
127 [],
128 [ 'name' => 'mockfm', 'status' => 400 ]
129 ],
130 'wrapped HTML format, with set status' => [
131 [],
132 // phpcs:ignore Generic.Files.LineLength.TooLong
133 '{"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}',
134 [ 'wrappedhtml' => 1 ],
135 [ 'name' => 'mockfm', 'status' => 400 ]
136 ],
137 'wrapped HTML format, cross-domain-policy' => [
138 [ 'continue' => '< CrOsS-DoMaIn-PoLiCy >' ],
139 // phpcs:ignore Generic.Files.LineLength.TooLong
140 '{"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}',
141 [ 'wrappedhtml' => 1 ],
142 [ 'name' => 'mockfm' ]
143 ],
144 ];
145 }
146
147 /**
148 * @dataProvider provideFilenameEncoding
149 */
150 public function testFilenameEncoding( $filename, $expect ) {
151 $ret = parent::encodeData( [], [], [
152 'name' => 'mock',
153 'class' => ApiFormatBase::class,
154 'factory' => function ( ApiMain $main, $format ) use ( $filename ) {
155 $mock = $this->getMockFormatter( $main, $format, [ 'getFilename' ] );
156 $mock->method( 'getFilename' )->willReturn( $filename );
157 return $mock;
158 },
159 'returnPrinter' => true,
160 ] );
161 $response = $ret['printer']->getMain()->getRequest()->response();
162
163 $this->assertSame( "inline; $expect", $response->getHeader( 'Content-Disposition' ) );
164 }
165
166 public static function provideFilenameEncoding() {
167 return [
168 'something simple' => [
169 'foo.xyz', 'filename=foo.xyz'
170 ],
171 'more complicated, but still simple' => [
172 'foo.!#$%&\'*+-^_`|~', 'filename=foo.!#$%&\'*+-^_`|~'
173 ],
174 'Needs quoting' => [
175 'foo\\bar.xyz', 'filename="foo\\\\bar.xyz"'
176 ],
177 'Needs quoting (2)' => [
178 'foo (bar).xyz', 'filename="foo (bar).xyz"'
179 ],
180 'Needs quoting (3)' => [
181 "foo\t\"b\x5car\"\0.xyz", "filename=\"foo\x5c\t\x5c\"b\x5c\x5car\x5c\"\x5c\0.xyz\""
182 ],
183 'Non-ASCII characters' => [
184 'fóo bár.🙌!',
185 "filename=\"f\xF3o b\xE1r.?!\"; filename*=UTF-8''f%C3%B3o%20b%C3%A1r.%F0%9F%99%8C!"
186 ]
187 ];
188 }
189
190 public function testBasics() {
191 $printer = $this->getMockFormatter( null, 'mock' );
192 $this->assertTrue( $printer->canPrintErrors() );
193 $this->assertSame(
194 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats',
195 $printer->getHelpUrls()
196 );
197 }
198
199 public function testDisable() {
200 $this->setMwGlobals( [
201 'wgApiFrameOptions' => 'DENY',
202 ] );
203
204 $printer = $this->getMockFormatter( null, 'mock' );
205 $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) {
206 $printer->printText( 'Foo' );
207 } );
208 $this->assertFalse( $printer->isDisabled() );
209 $printer->disable();
210 $this->assertTrue( $printer->isDisabled() );
211
212 $printer->setHttpStatus( 400 );
213 $printer->initPrinter();
214 $printer->execute();
215 ob_start();
216 $printer->closePrinter();
217 $this->assertSame( '', ob_get_clean() );
218 $response = $printer->getMain()->getRequest()->response();
219 $this->assertNull( $response->getHeader( 'Content-Type' ) );
220 $this->assertNull( $response->getHeader( 'X-Frame-Options' ) );
221 $this->assertNull( $response->getHeader( 'Content-Disposition' ) );
222 $this->assertNull( $response->getStatusCode() );
223 }
224
225 public function testNullMimeType() {
226 $this->setMwGlobals( [
227 'wgApiFrameOptions' => 'DENY',
228 ] );
229
230 $printer = $this->getMockFormatter( null, 'mock', [ 'getMimeType' ] );
231 $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) {
232 $printer->printText( 'Foo' );
233 } );
234 $printer->method( 'getMimeType' )->willReturn( null );
235 $this->assertNull( $printer->getMimeType(), 'sanity check' );
236
237 $printer->initPrinter();
238 $printer->execute();
239 ob_start();
240 $printer->closePrinter();
241 $this->assertSame( 'Foo', ob_get_clean() );
242 $response = $printer->getMain()->getRequest()->response();
243 $this->assertNull( $response->getHeader( 'Content-Type' ) );
244 $this->assertNull( $response->getHeader( 'X-Frame-Options' ) );
245 $this->assertNull( $response->getHeader( 'Content-Disposition' ) );
246
247 $printer = $this->getMockFormatter( null, 'mockfm', [ 'getMimeType' ] );
248 $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) {
249 $printer->printText( 'Foo' );
250 } );
251 $printer->method( 'getMimeType' )->willReturn( null );
252 $this->assertNull( $printer->getMimeType(), 'sanity check' );
253 $this->assertTrue( $printer->getIsHtml(), 'sanity check' );
254
255 $printer->initPrinter();
256 $printer->execute();
257 ob_start();
258 $printer->closePrinter();
259 $this->assertSame( 'Foo', ob_get_clean() );
260 $response = $printer->getMain()->getRequest()->response();
261 $this->assertSame(
262 'text/html; charset=utf-8', strtolower( $response->getHeader( 'Content-Type' ) )
263 );
264 $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) );
265 $this->assertSame(
266 'inline; filename=api-result.html', $response->getHeader( 'Content-Disposition' )
267 );
268 }
269
270 public function testApiFrameOptions() {
271 $this->setMwGlobals( [ 'wgApiFrameOptions' => 'DENY' ] );
272 $printer = $this->getMockFormatter( null, 'mock' );
273 $printer->initPrinter();
274 $this->assertSame(
275 'DENY',
276 $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
277 );
278
279 $this->setMwGlobals( [ 'wgApiFrameOptions' => 'SAMEORIGIN' ] );
280 $printer = $this->getMockFormatter( null, 'mock' );
281 $printer->initPrinter();
282 $this->assertSame(
283 'SAMEORIGIN',
284 $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
285 );
286
287 $this->setMwGlobals( [ 'wgApiFrameOptions' => false ] );
288 $printer = $this->getMockFormatter( null, 'mock' );
289 $printer->initPrinter();
290 $this->assertNull(
291 $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' )
292 );
293 }
294
295 public function testForceDefaultParams() {
296 $context = new RequestContext;
297 $context->setRequest( new FauxRequest( [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], true ) );
298 $main = new ApiMain( $context );
299 $allowedParams = [
300 'foo' => [],
301 'bar' => [ ApiBase::PARAM_DFLT => 'bar?' ],
302 'baz' => 'baz!',
303 ];
304
305 $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] );
306 $printer->method( 'getAllowedParams' )->willReturn( $allowedParams );
307 $this->assertEquals(
308 [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ],
309 $printer->extractRequestParams(),
310 'sanity check'
311 );
312
313 $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] );
314 $printer->method( 'getAllowedParams' )->willReturn( $allowedParams );
315 $printer->forceDefaultParams();
316 $this->assertEquals(
317 [ 'foo' => null, 'bar' => 'bar?', 'baz' => 'baz!' ],
318 $printer->extractRequestParams()
319 );
320 }
321
322 public function testGetAllowedParams() {
323 $printer = $this->getMockFormatter( null, 'mock' );
324 $this->assertSame( [], $printer->getAllowedParams() );
325
326 $printer = $this->getMockFormatter( null, 'mockfm' );
327 $this->assertSame( [
328 'wrappedhtml' => [
329 ApiBase::PARAM_DFLT => false,
330 ApiBase::PARAM_HELP_MSG => 'apihelp-format-param-wrappedhtml',
331 ]
332 ], $printer->getAllowedParams() );
333 }
334
335 public function testGetExamplesMessages() {
336 $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mock' ) );
337 $this->assertSame( [
338 'action=query&meta=siteinfo&siprop=namespaces&format=mock'
339 => [ 'apihelp-format-example-generic', 'MOCK' ]
340 ], $printer->getExamplesMessages() );
341
342 $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mockfm' ) );
343 $this->assertSame( [
344 'action=query&meta=siteinfo&siprop=namespaces&format=mockfm'
345 => [ 'apihelp-format-example-generic', 'MOCK' ]
346 ], $printer->getExamplesMessages() );
347 }
348
349 /**
350 * @dataProvider provideHtmlHeader
351 */
352 public function testHtmlHeader( $post, $registerNonHtml, $expect ) {
353 $context = new RequestContext;
354 $request = new FauxRequest( [ 'a' => 1, 'b' => 2 ], $post );
355 $request->setRequestURL( 'http://example.org/wx/api.php' );
356 $context->setRequest( $request );
357 $context->setLanguage( 'qqx' );
358 $main = new ApiMain( $context );
359 $printer = $this->getMockFormatter( $main, 'mockfm' );
360 $mm = $printer->getMain()->getModuleManager();
361 $mm->addModule( 'mockfm', 'format', ApiFormatBase::class, function () {
362 return $mock;
363 } );
364 if ( $registerNonHtml ) {
365 $mm->addModule( 'mock', 'format', ApiFormatBase::class, function () {
366 return $mock;
367 } );
368 }
369
370 $printer->initPrinter();
371 $printer->execute();
372 ob_start();
373 $printer->closePrinter();
374 $text = ob_get_clean();
375 $this->assertContains( $expect, $text );
376 }
377
378 public static function provideHtmlHeader() {
379 return [
380 [ false, false, '(api-format-prettyprint-header-only-html: MOCK)' ],
381 [ true, false, '(api-format-prettyprint-header-only-html: MOCK)' ],
382 // phpcs:ignore Generic.Files.LineLength.TooLong
383 [ 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>)' ],
384 [ true, true, '(api-format-prettyprint-header: MOCK, mock)' ],
385 ];
386 }
387
388 }