ApiFormatBaseTest.php 13 KB

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