JsonApiTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. use PrivateBin\Controller;
  4. use PrivateBin\Data\Filesystem;
  5. use PrivateBin\Persistence\ServerSalt;
  6. use PrivateBin\Request;
  7. class JsonApiTest extends TestCase
  8. {
  9. protected $_model;
  10. protected $_path;
  11. public function setUp(): void
  12. {
  13. /* Setup Routine */
  14. $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
  15. if (!is_dir($this->_path)) {
  16. mkdir($this->_path);
  17. }
  18. $this->_model = new Filesystem(['dir' => $this->_path]);
  19. ServerSalt::setStore($this->_model);
  20. $_POST = [];
  21. $_GET = [];
  22. $_SERVER = [];
  23. if ($this->_model->exists(Helper::getPasteId())) {
  24. $this->_model->delete(Helper::getPasteId());
  25. }
  26. $options = parse_ini_file(CONF_SAMPLE, true);
  27. $options['model_options']['dir'] = $this->_path;
  28. Helper::confBackup();
  29. Helper::createIniFile(CONF, $options);
  30. }
  31. public function tearDown(): void
  32. {
  33. /* Tear Down Routine */
  34. unlink(CONF);
  35. Helper::confRestore();
  36. Helper::rmDir($this->_path);
  37. }
  38. /**
  39. * @runInSeparateProcess
  40. */
  41. public function testCreate()
  42. {
  43. $options = parse_ini_file(CONF, true);
  44. $options['traffic']['limit'] = 0;
  45. Helper::createIniFile(CONF, $options);
  46. $paste = Helper::getPasteJson();
  47. $file = Helper::createTempFile();
  48. file_put_contents($file, $paste);
  49. Request::setInputStream($file);
  50. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  51. $_SERVER['REQUEST_METHOD'] = 'POST';
  52. $_SERVER['REMOTE_ADDR'] = '::1';
  53. $_SERVER['REQUEST_URI'] = '/';
  54. ob_start();
  55. new Controller;
  56. $content = ob_get_contents();
  57. ob_end_clean();
  58. $response = json_decode($content, true);
  59. $this->assertEquals(0, $response['status'], 'outputs status');
  60. $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
  61. $this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
  62. $paste = $this->_model->read($response['id']);
  63. $this->assertEquals(
  64. hash_hmac('sha256', $response['id'], $paste['meta']['salt']),
  65. $response['deletetoken'],
  66. 'outputs valid delete token'
  67. );
  68. }
  69. /**
  70. * @runInSeparateProcess
  71. */
  72. public function testPut()
  73. {
  74. $options = parse_ini_file(CONF, true);
  75. $options['traffic']['limit'] = 0;
  76. Helper::createIniFile(CONF, $options);
  77. $paste = Helper::getPasteJson();
  78. $file = Helper::createTempFile();
  79. file_put_contents($file, $paste);
  80. Request::setInputStream($file);
  81. $_SERVER['QUERY_STRING'] = Helper::getPasteId();
  82. $_GET[Helper::getPasteId()] = '';
  83. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  84. $_SERVER['REQUEST_METHOD'] = 'PUT';
  85. $_SERVER['REMOTE_ADDR'] = '::1';
  86. ob_start();
  87. new Controller;
  88. $content = ob_get_contents();
  89. ob_end_clean();
  90. unlink($file);
  91. $response = json_decode($content, true);
  92. $this->assertEquals(0, $response['status'], 'outputs status');
  93. $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputted paste ID matches input');
  94. $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
  95. $this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
  96. $paste = $this->_model->read($response['id']);
  97. $this->assertEquals(
  98. hash_hmac('sha256', $response['id'], $paste['meta']['salt']),
  99. $response['deletetoken'],
  100. 'outputs valid delete token'
  101. );
  102. }
  103. /**
  104. * @runInSeparateProcess
  105. */
  106. public function testDelete()
  107. {
  108. $data = Helper::getPaste();
  109. $this->_model->create(Helper::getPasteId(), $data);
  110. $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
  111. $paste = $this->_model->read(Helper::getPasteId());
  112. $file = Helper::createTempFile();
  113. file_put_contents($file, json_encode([
  114. 'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']),
  115. ]));
  116. Request::setInputStream($file);
  117. $_SERVER['QUERY_STRING'] = Helper::getPasteId();
  118. $_GET[Helper::getPasteId()] = '';
  119. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  120. $_SERVER['REQUEST_METHOD'] = 'DELETE';
  121. ob_start();
  122. new Controller;
  123. $content = ob_get_contents();
  124. ob_end_clean();
  125. unlink($file);
  126. $response = json_decode($content, true);
  127. $this->assertEquals(0, $response['status'], 'outputs status');
  128. $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');
  129. }
  130. /**
  131. * @runInSeparateProcess
  132. */
  133. public function testDeleteWithPost()
  134. {
  135. $data = Helper::getPaste();
  136. $this->_model->create(Helper::getPasteId(), $data);
  137. $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
  138. $paste = $this->_model->read(Helper::getPasteId());
  139. $file = Helper::createTempFile();
  140. file_put_contents($file, json_encode([
  141. 'pasteid' => Helper::getPasteId(),
  142. 'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']),
  143. ]));
  144. Request::setInputStream($file);
  145. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  146. $_SERVER['REQUEST_METHOD'] = 'POST';
  147. ob_start();
  148. new Controller;
  149. $content = ob_get_contents();
  150. ob_end_clean();
  151. $response = json_decode($content, true);
  152. $this->assertEquals(0, $response['status'], 'outputs status');
  153. $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');
  154. }
  155. /**
  156. * @runInSeparateProcess
  157. */
  158. public function testRead()
  159. {
  160. $paste = Helper::getPaste();
  161. $this->_model->create(Helper::getPasteId(), $paste);
  162. $_SERVER['QUERY_STRING'] = Helper::getPasteId();
  163. $_GET[Helper::getPasteId()] = '';
  164. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  165. ob_start();
  166. new Controller;
  167. $content = ob_get_contents();
  168. ob_end_clean();
  169. $response = json_decode($content, true);
  170. $this->assertEquals(0, $response['status'], 'outputs success status');
  171. $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs data correctly');
  172. $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
  173. $this->assertEquals($paste['ct'], $response['ct'], 'outputs data correctly');
  174. $this->assertFalse(array_key_exists('created', $paste['meta']), 'does not output created');
  175. $this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
  176. $this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
  177. }
  178. /**
  179. * @runInSeparateProcess
  180. */
  181. public function testJsonLdPaste()
  182. {
  183. $_GET['jsonld'] = 'paste';
  184. ob_start();
  185. new Controller;
  186. $content = ob_get_contents();
  187. ob_end_clean();
  188. $this->assertEquals(str_replace(
  189. '?jsonld=',
  190. '/?jsonld=',
  191. file_get_contents(PUBLIC_PATH . '/js/paste.jsonld')
  192. ), $content, 'outputs data correctly');
  193. }
  194. /**
  195. * @runInSeparateProcess
  196. */
  197. public function testJsonLdComment()
  198. {
  199. $_GET['jsonld'] = 'comment';
  200. ob_start();
  201. new Controller;
  202. $content = ob_get_contents();
  203. ob_end_clean();
  204. $this->assertEquals(str_replace(
  205. '?jsonld=',
  206. '/?jsonld=',
  207. file_get_contents(PUBLIC_PATH . '/js/comment.jsonld')
  208. ), $content, 'outputs data correctly');
  209. }
  210. /**
  211. * @runInSeparateProcess
  212. */
  213. public function testJsonLdPasteMeta()
  214. {
  215. $_GET['jsonld'] = 'pastemeta';
  216. ob_start();
  217. new Controller;
  218. $content = ob_get_contents();
  219. ob_end_clean();
  220. $this->assertEquals(str_replace(
  221. '?jsonld=',
  222. '/?jsonld=',
  223. file_get_contents(PUBLIC_PATH . '/js/pastemeta.jsonld')
  224. ), $content, 'outputs data correctly');
  225. }
  226. /**
  227. * @runInSeparateProcess
  228. */
  229. public function testJsonLdCommentMeta()
  230. {
  231. $_GET['jsonld'] = 'commentmeta';
  232. ob_start();
  233. new Controller;
  234. $content = ob_get_contents();
  235. ob_end_clean();
  236. $this->assertEquals(str_replace(
  237. '?jsonld=',
  238. '/?jsonld=',
  239. file_get_contents(PUBLIC_PATH . '/js/commentmeta.jsonld')
  240. ), $content, 'outputs data correctly');
  241. }
  242. /**
  243. * @runInSeparateProcess
  244. */
  245. public function testJsonLdTypes()
  246. {
  247. $_GET['jsonld'] = 'types';
  248. ob_start();
  249. new Controller;
  250. $content = ob_get_contents();
  251. ob_end_clean();
  252. $this->assertEquals(str_replace(
  253. '?jsonld=',
  254. '/?jsonld=',
  255. file_get_contents(PUBLIC_PATH . '/js/types.jsonld')
  256. ), $content, 'outputs data correctly');
  257. }
  258. /**
  259. * @runInSeparateProcess
  260. */
  261. public function testJsonLdInvalid()
  262. {
  263. $_GET['jsonld'] = CONF;
  264. ob_start();
  265. new Controller;
  266. $content = ob_get_contents();
  267. ob_end_clean();
  268. $this->assertEquals('{}', $content, 'does not output nasty data');
  269. }
  270. /**
  271. * @runInSeparateProcess
  272. * @dataProvider baseUriProvider
  273. */
  274. public function testShortenViaYourls($baseUri)
  275. {
  276. $mock_yourls_service = $this->_path . DIRECTORY_SEPARATOR . 'yourls.json';
  277. $options = parse_ini_file(CONF, true);
  278. $options['main']['basepath'] = 'https://example.com/path'; // missing slash gets added by Configuration constructor
  279. $options['main']['urlshortener'] = 'https://example.com' . $baseUri . 'link=';
  280. $options['yourls']['apiurl'] = $mock_yourls_service;
  281. Helper::createIniFile(CONF, $options);
  282. // the real service answer is more complex, but we only look for the shorturl & statusCode
  283. file_put_contents($mock_yourls_service, '{"shorturl":"https:\/\/example.com\/1","statusCode":200}');
  284. $_SERVER['REQUEST_URI'] = $baseUri . 'link=https%3A%2F%2Fexample.com%2Fpath%2F%3Ffoo%23bar';
  285. $_GET['link'] = 'https://example.com/path/?foo#bar';
  286. if (str_contains($baseUri, '?shortenviayourls')) {
  287. $_GET['shortenviayourls'] = null;
  288. }
  289. ob_start();
  290. new Controller;
  291. $content = ob_get_contents();
  292. ob_end_clean();
  293. $this->assertStringContainsString('id="pasteurl" href="https://example.com/1"', $content, "'{$baseUri}' outputs shortened URL correctly");
  294. }
  295. /**
  296. * @runInSeparateProcess
  297. * @dataProvider baseShlinkUriProvider
  298. */
  299. public function testShortenViaShlink($baseUri)
  300. {
  301. $mock_shlink_service = $this->_path . DIRECTORY_SEPARATOR . 'shlink.json';
  302. $options = parse_ini_file(CONF, true);
  303. $options['main']['basepath'] = 'https://example.com/path'; // missing slash gets added by Configuration constructor
  304. $options['main']['urlshortener'] = 'https://example.com' . $baseUri . 'link=';
  305. $options['shlink']['apiurl'] = $mock_shlink_service;
  306. Helper::createIniFile(CONF, $options);
  307. // the real service answer is more complex, but we only look for the shorturl & statusCode
  308. file_put_contents($mock_shlink_service, '{"shortUrl":"https:\/\/example.com\/1"}');
  309. $_SERVER['REQUEST_URI'] = $baseUri . 'link=https%3A%2F%2Fexample.com%2Fpath%2F%3Ffoo%23bar';
  310. $_GET['link'] = 'https://example.com/path/?foo#bar';
  311. if (str_contains($baseUri, '?shortenviashlink')) {
  312. $_GET['shortenviashlink'] = null;
  313. }
  314. ob_start();
  315. new Controller;
  316. $content = ob_get_contents();
  317. ob_end_clean();
  318. $this->assertStringContainsString('id="pasteurl" href="https://example.com/1"', $content, "'{$baseUri}' outputs shortened URL correctly");
  319. }
  320. /**
  321. * @runInSeparateProcess
  322. * @dataProvider baseShlinkUriProvider
  323. */
  324. public function testShortenViaShlinkFailureHttp($baseUri)
  325. {
  326. $mock_shlink_service = 'https://httpbin.org/status/403';
  327. $options = parse_ini_file(CONF, true);
  328. $options['main']['basepath'] = 'https://example.com/path'; // missing slash gets added by Configuration constructor
  329. $options['main']['urlshortener'] = 'https://example.com' . $baseUri . 'link=';
  330. $options['shlink']['apiurl'] = $mock_shlink_service;
  331. Helper::createIniFile(CONF, $options);
  332. $_SERVER['REQUEST_URI'] = $baseUri . 'link=https%3A%2F%2Fexample.com%2Fpath%2F%3Ffoo%23bar';
  333. $_GET['link'] = 'https://example.com/path/?foo#bar';
  334. if (str_contains($baseUri, '?shortenviashlink')) {
  335. $_GET['shortenviashlink'] = null;
  336. }
  337. ob_start();
  338. // Use @ to ignore the http warning for the 403. It will be handled appropriately by AbstractProxy
  339. @new Controller;
  340. $content = ob_get_contents();
  341. ob_end_clean();
  342. $this->assertStringContainsString('Proxy error: Bad response.', $content, 'outputs error correctly');
  343. }
  344. /**
  345. * @runInSeparateProcess
  346. * @dataProvider baseShlinkUriProvider
  347. */
  348. public function testShortenViaShlinkSuccessButMissingShortUrl($baseUri)
  349. {
  350. $mock_shlink_service = $this->_path . DIRECTORY_SEPARATOR . 'shlink.json';
  351. $options = parse_ini_file(CONF, true);
  352. $options['main']['basepath'] = 'https://example.com/path'; // missing slash gets added by Configuration constructor
  353. $options['main']['urlshortener'] = 'https://example.com' . $baseUri . 'link=';
  354. $options['shlink']['apiurl'] = $mock_shlink_service;
  355. Helper::createIniFile(CONF, $options);
  356. // Ideally, this should never happen, just in case "shortUrl" is somehow missing in the 200 response
  357. file_put_contents($mock_shlink_service, '{}');
  358. $_SERVER['REQUEST_URI'] = $baseUri . 'link=https%3A%2F%2Fexample.com%2Fpath%2F%3Ffoo%23bar';
  359. $_GET['link'] = 'https://example.com/path/?foo#bar';
  360. if (str_contains($baseUri, '?shortenviashlink')) {
  361. $_GET['shortenviashlink'] = null;
  362. }
  363. ob_start();
  364. new Controller;
  365. $content = ob_get_contents();
  366. ob_end_clean();
  367. $this->assertStringContainsString('Proxy error: Error parsing proxy response.', $content, 'outputs error correctly');
  368. }
  369. public function baseUriProvider()
  370. {
  371. return [
  372. ['/path/shortenviayourls?'],
  373. ['/path/index.php/shortenviayourls?'],
  374. ['/path?shortenviayourls&'],
  375. ];
  376. }
  377. public function baseShlinkUriProvider()
  378. {
  379. return [
  380. ['/path/shortenviashlink?'],
  381. ['/path/index.php/shortenviashlink?'],
  382. ['/path?shortenviashlink&'],
  383. ];
  384. }
  385. /**
  386. * @runInSeparateProcess
  387. */
  388. public function testShortenViaYourlsFailure()
  389. {
  390. $options = parse_ini_file(CONF, true);
  391. $options['main']['basepath'] = 'https://example.com/path'; // missing slash gets added by Configuration constructor
  392. Helper::createIniFile(CONF, $options);
  393. $_SERVER['REQUEST_URI'] = '/path/shortenviayourls?link=https%3A%2F%2Fexample.com%2Fpath%2F%3Ffoo%23bar';
  394. $_GET['link'] = 'https://example.com/path/?foo#bar';
  395. ob_start();
  396. new Controller;
  397. $content = ob_get_contents();
  398. ob_end_clean();
  399. $this->assertStringContainsString('Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.', $content, 'outputs error correctly');
  400. }
  401. /**
  402. * @runInSeparateProcess
  403. */
  404. public function testShortenViaShlinkFailure()
  405. {
  406. $options = parse_ini_file(CONF, true);
  407. $options['main']['basepath'] = 'https://example.com/path'; // missing slash gets added by Configuration constructor
  408. Helper::createIniFile(CONF, $options);
  409. $_SERVER['REQUEST_URI'] = '/path/shortenviashlink?link=https%3A%2F%2Fexample.com%2Fpath%2F%3Ffoo%23bar';
  410. $_GET['link'] = 'https://example.com/path/?foo#bar';
  411. ob_start();
  412. new Controller;
  413. $content = ob_get_contents();
  414. ob_end_clean();
  415. $this->assertStringContainsString('Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.', $content, 'outputs error correctly');
  416. }
  417. }