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