3 declare(strict_types
=1);
5 namespace PhpMyAdmin\Tests
;
8 use PhpMyAdmin\Http\ServerRequest
;
9 use PhpMyAdmin\ResponseRenderer
;
10 use PhpMyAdmin\Sanitize
;
15 use function _pgettext
;
17 use function htmlspecialchars
;
18 use function mb_strpos
;
19 use function ob_end_clean
;
20 use function ob_get_contents
;
21 use function ob_start
;
22 use function preg_quote
;
23 use function serialize
;
24 use function str_repeat
;
27 * @covers \PhpMyAdmin\Core
29 class CoreTest
extends AbstractNetworkTestCase
32 * Setup for test cases
34 protected function setUp(): void
38 parent
::setLanguage();
40 $GLOBALS['server'] = 0;
42 $GLOBALS['table'] = '';
43 $GLOBALS['PMA_PHP_SELF'] = 'http://example.net/';
44 $GLOBALS['config']->set('URLQueryEncryption', false);
48 * Test for Core::arrayRead
50 public function testArrayRead(): void
79 Core
::arrayRead('int', $arr),
84 Core
::arrayRead('str', $arr),
89 Core
::arrayRead('arr/0', $arr),
94 Core
::arrayRead('arr/1', $arr),
99 Core
::arrayRead('arr/2', $arr),
104 Core
::arrayRead('sarr/arr1/0', $arr),
105 $arr['sarr']['arr1'][0]
109 Core
::arrayRead('sarr/arr1/1', $arr),
110 $arr['sarr']['arr1'][1]
114 Core
::arrayRead('sarr/arr1/2', $arr),
115 $arr['sarr']['arr1'][2]
119 Core
::arrayRead('sarr/0/0', $arr),
124 Core
::arrayRead('sarr/0/1', $arr),
129 Core
::arrayRead('sarr/0/1/2', $arr),
130 $arr['sarr'][0][1][2]
134 Core
::arrayRead('sarr/not_exiting/1', $arr),
139 Core
::arrayRead('sarr/not_exiting/1', $arr, 0),
144 Core
::arrayRead('sarr/not_exiting/1', $arr, 'default_val'),
150 * Test for Core::arrayWrite
152 public function testArrayWrite(): void
180 Core
::arrayWrite('int', $arr, 5);
181 $this->assertEquals($arr['int'], 5);
183 Core
::arrayWrite('str', $arr, '_str');
184 $this->assertEquals($arr['str'], '_str');
186 Core
::arrayWrite('arr/0', $arr, 'val_arr_0');
187 $this->assertEquals($arr['arr'][0], 'val_arr_0');
189 Core
::arrayWrite('arr/1', $arr, 'val_arr_1');
190 $this->assertEquals($arr['arr'][1], 'val_arr_1');
192 Core
::arrayWrite('arr/2', $arr, 'val_arr_2');
193 $this->assertEquals($arr['arr'][2], 'val_arr_2');
195 Core
::arrayWrite('sarr/arr1/0', $arr, 'val_sarr_arr_0');
196 $this->assertEquals($arr['sarr']['arr1'][0], 'val_sarr_arr_0');
198 Core
::arrayWrite('sarr/arr1/1', $arr, 'val_sarr_arr_1');
199 $this->assertEquals($arr['sarr']['arr1'][1], 'val_sarr_arr_1');
201 Core
::arrayWrite('sarr/arr1/2', $arr, 'val_sarr_arr_2');
202 $this->assertEquals($arr['sarr']['arr1'][2], 'val_sarr_arr_2');
204 Core
::arrayWrite('sarr/0/0', $arr, 5);
205 $this->assertEquals($arr['sarr'][0][0], 5);
207 Core
::arrayWrite('sarr/0/1/0', $arr, 'e');
208 $this->assertEquals($arr['sarr'][0][1][0], 'e');
210 Core
::arrayWrite('sarr/not_existing/1', $arr, 'some_val');
211 $this->assertEquals($arr['sarr']['not_existing'][1], 'some_val');
213 Core
::arrayWrite('sarr/0/2', $arr, null);
214 $this->assertNull($arr['sarr'][0][2]);
218 * Test for Core::arrayRemove
220 public function testArrayRemove(): void
248 Core
::arrayRemove('int', $arr);
249 $this->assertArrayNotHasKey('int', $arr);
251 Core
::arrayRemove('str', $arr);
252 $this->assertArrayNotHasKey('str', $arr);
254 Core
::arrayRemove('arr/0', $arr);
255 $this->assertArrayNotHasKey(0, $arr['arr']);
257 Core
::arrayRemove('arr/1', $arr);
258 $this->assertArrayNotHasKey(1, $arr['arr']);
260 Core
::arrayRemove('arr/2', $arr);
261 $this->assertArrayNotHasKey('arr', $arr);
264 Core
::arrayRemove('sarr/not_existing/1', $arr);
265 $this->assertEquals($tmp_arr, $arr);
267 Core
::arrayRemove('sarr/arr1/0', $arr);
268 $this->assertArrayNotHasKey(0, $arr['sarr']['arr1']);
270 Core
::arrayRemove('sarr/arr1/1', $arr);
271 $this->assertArrayNotHasKey(1, $arr['sarr']['arr1']);
273 Core
::arrayRemove('sarr/arr1/2', $arr);
274 $this->assertArrayNotHasKey('arr1', $arr['sarr']);
276 Core
::arrayRemove('sarr/0/0', $arr);
277 $this->assertArrayNotHasKey(0, $arr['sarr'][0]);
279 Core
::arrayRemove('sarr/0/1/0', $arr);
280 $this->assertArrayNotHasKey(0, $arr['sarr'][0][1]);
282 Core
::arrayRemove('sarr/0/1/1', $arr);
283 $this->assertArrayNotHasKey(1, $arr['sarr'][0][1]);
285 Core
::arrayRemove('sarr/0/1/2', $arr);
286 $this->assertArrayNotHasKey(1, $arr['sarr'][0]);
288 Core
::arrayRemove('sarr/0/2', $arr);
290 $this->assertEmpty($arr);
294 * Test for Core::checkPageValidity
296 * @param string|null $page Page
297 * @param array $allowList Allow list
298 * @param bool $include whether the page is going to be included
299 * @param bool $expected Expected value
301 * @dataProvider providerTestGotoNowhere
303 public function testGotoNowhere(?
string $page, array $allowList, bool $include, bool $expected): void
305 $this->assertSame($expected, Core
::checkPageValidity($page, $allowList, $include));
309 * Data provider for testGotoNowhere
313 public function providerTestGotoNowhere(): array
341 'index.php?sql.php&test=true',
347 'index.php?sql.php&test=true',
353 'index.php%3Fsql.php%26test%3Dtrue',
359 'index.php%3Fsql.php%26test%3Dtrue',
368 * Test for Core::fatalError
370 public function testFatalErrorMessage(): void
373 ResponseRenderer
::getInstance()->setAjax(false);
375 $this->expectOutputRegex('/FatalError!/');
376 Core
::fatalError('FatalError!');
380 * Test for Core::fatalError
382 public function testFatalErrorMessageWithArgs(): void
385 ResponseRenderer
::getInstance()->setAjax(false);
387 $message = 'Fatal error #%d in file %s.';
393 $this->expectOutputRegex('/Fatal error #1 in file error_file.php./');
394 Core
::fatalError($message, $params);
396 $message = 'Fatal error in file %s.';
397 $params = 'error_file.php';
399 $this->expectOutputRegex('/Fatal error in file error_file.php./');
400 Core
::fatalError($message, $params);
404 * Test for Core::getRealSize
406 * @param string $size Size
407 * @param int $expected Expected value
409 * @group 32bit-incompatible
411 * @dataProvider providerTestGetRealSize
413 public function testGetRealSize(string $size, int $expected): void
415 $this->assertEquals($expected, Core
::getRealSize($size));
419 * Data provider for testGetRealSize
423 public function providerTestGetRealSize(): array
444 12 * 1024 * 1024 * 1024,
452 8 * 1000 * 1024 * 1024,
456 8 * 1024 * 1024 * 1024,
478 * Test for Core::getPHPDocLink
480 public function testGetPHPDocLink(): void
482 $lang = _pgettext('PHP documentation language', 'en');
484 Core
::getPHPDocLink('function'),
485 './url.php?url=https%3A%2F%2Fwww.php.net%2Fmanual%2F'
486 . $lang . '%2Ffunction'
491 * Test for Core::linkURL
493 * @param string $link URL where to go
494 * @param string $url Expected value
496 * @dataProvider providerTestLinkURL
498 public function testLinkURL(string $link, string $url): void
500 $this->assertEquals(Core
::linkURL($link), $url);
504 * Data provider for testLinkURL
508 public function providerTestLinkURL(): array
512 'https://wiki.phpmyadmin.net',
513 './url.php?url=https%3A%2F%2Fwiki.phpmyadmin.net',
516 'https://wiki.phpmyadmin.net',
517 './url.php?url=https%3A%2F%2Fwiki.phpmyadmin.net',
520 'wiki.phpmyadmin.net',
521 'wiki.phpmyadmin.net',
524 'index.php?db=phpmyadmin',
525 'index.php?db=phpmyadmin',
531 * Test for Core::sendHeaderLocation
533 public function testSendHeaderLocationWithoutSidWithIis(): void
535 $GLOBALS['server'] = 0;
536 $GLOBALS['config']->set('PMA_IS_IIS', true);
538 $testUri = 'https://example.com/test.php';
540 $this->mockResponse('Location: ' . $testUri);
541 Core
::sendHeaderLocation($testUri); // sets $GLOBALS['header']
543 $this->mockResponse('Refresh: 0; ' . $testUri);
544 Core
::sendHeaderLocation($testUri, true); // sets $GLOBALS['header']
548 * Test for Core::sendHeaderLocation
550 public function testSendHeaderLocationWithoutSidWithoutIis(): void
552 $GLOBALS['server'] = 0;
553 parent
::setGlobalConfig();
554 $GLOBALS['config']->set('PMA_IS_IIS', null);
556 $testUri = 'https://example.com/test.php';
558 $this->mockResponse('Location: ' . $testUri);
559 Core
::sendHeaderLocation($testUri); // sets $GLOBALS['header']
563 * Test for Core::sendHeaderLocation
565 public function testSendHeaderLocationIisLongUri(): void
567 $GLOBALS['server'] = 0;
568 parent
::setGlobalConfig();
569 $GLOBALS['config']->set('PMA_IS_IIS', true);
572 $testUri = 'https://example.com/test.php?testlonguri=over600chars&test=test'
573 . '&test=test&test=test&test=test&test=test&test=test&test=test'
574 . '&test=test&test=test&test=test&test=test&test=test&test=test'
575 . '&test=test&test=test&test=test&test=test&test=test&test=test'
576 . '&test=test&test=test&test=test&test=test&test=test&test=test'
577 . '&test=test&test=test&test=test&test=test&test=test&test=test'
578 . '&test=test&test=test&test=test&test=test&test=test&test=test'
579 . '&test=test&test=test&test=test&test=test&test=test&test=test'
580 . '&test=test&test=test&test=test&test=test&test=test&test=test'
581 . '&test=test&test=test&test=test&test=test&test=test&test=test'
582 . '&test=test&test=test';
583 $testUri_html = htmlspecialchars($testUri);
584 $testUri_js = Sanitize
::escapeJsString($testUri);
586 $header = "<html>\n<head>\n <title>- - -</title>"
587 . "\n <meta http-equiv=\"expires\" content=\"0\">"
588 . "\n <meta http-equiv=\"Pragma\" content=\"no-cache\">"
589 . "\n <meta http-equiv=\"Cache-Control\" content=\"no-cache\">"
590 . "\n <meta http-equiv=\"Refresh\" content=\"0;url=" . $testUri_html . '">'
591 . "\n <script type=\"text/javascript\">\n //<![CDATA["
592 . "\n setTimeout(function() { window.location = decodeURI('" . $testUri_js . "'); }, 2000);"
593 . "\n //]]>\n </script>\n</head>"
594 . "\n<body>\n<script type=\"text/javascript\">\n //<![CDATA["
595 . "\n document.write('<p><a href=\"" . $testUri_html . '">' . __('Go') . "</a></p>');"
596 . "\n //]]>\n</script>\n</body>\n</html>\n";
598 $this->expectOutputString($header);
600 $this->mockResponse();
602 Core
::sendHeaderLocation($testUri);
606 * Test for unserializing
608 * @param string $url URL to test
609 * @param mixed $expected Expected result
611 * @dataProvider provideTestIsAllowedDomain
613 public function testIsAllowedDomain(string $url, $expected): void
615 $_SERVER['SERVER_NAME'] = 'server.local';
618 Core
::isAllowedDomain($url)
627 public function provideTestIsAllowedDomain(): array
631 'https://www.phpmyadmin.net/',
635 'http://duckduckgo.com\\@github.com',
639 'https://github.com/',
643 'https://github.com:123/',
647 'https://user:pass@github.com:123/',
651 'https://user:pass@github.com/',
655 'https://server.local/',
666 * Test for unserializing
668 * @param string $data Serialized data
669 * @param mixed $expected Expected result
671 * @dataProvider provideTestSafeUnserialize
673 public function testSafeUnserialize(string $data, $expected): void
677 Core
::safeUnserialize($data)
686 public function provideTestSafeUnserialize(): array
702 'O:1:"a":1:{s:5:"value";s:3:"100";}',
706 'O:8:"stdClass":1:{s:5:"field";O:8:"stdClass":0:{}}',
710 'a:2:{i:0;s:90:"1234567890;a3456789012345678901234567890123456789012'
711 . '34567890123456789012345678901234567890";i:1;O:8:"stdClass":0:{}}',
715 serialize([1, 2, 3]),
723 serialize('string""'),
727 serialize(['foo' => 'bar']),
731 serialize(['1', new stdClass(), '2']),
738 * Test for MySQL host sanitizing
740 * @param string $host Test host name
741 * @param string $expected Expected result
743 * @dataProvider provideTestSanitizeMySQLHost
745 public function testSanitizeMySQLHost(string $host, string $expected): void
749 Core
::sanitizeMySQLHost($host)
758 public function provideTestSanitizeMySQLHost(): array
781 * Test for replacing dots.
783 public function testReplaceDots(): void
786 Core
::securePath('../../../etc/passwd'),
790 Core
::securePath('/var/www/../phpmyadmin'),
791 '/var/www/./phpmyadmin'
794 Core
::securePath('./path/with..dots/../../file..php'),
795 './path/with.dots/././file.php'
800 * Test for Core::warnMissingExtension
802 public function testMissingExtensionFatal(): void
805 ResponseRenderer
::getInstance()->setAjax(false);
808 $warn = 'The <a href="' . Core
::getPHPDocLink('book.' . $ext . '.php')
809 . '" target="Documentation"><em>' . $ext
810 . '</em></a> extension is missing. Please check your PHP configuration.';
812 $this->expectOutputRegex('@' . preg_quote($warn, '@') . '@');
814 Core
::warnMissingExtension($ext, true);
818 * Test for Core::warnMissingExtension
820 public function testMissingExtensionFatalWithExtra(): void
823 ResponseRenderer
::getInstance()->setAjax(false);
826 $extra = 'Appended Extra String';
828 $warn = 'The <a href="' . Core
::getPHPDocLink('book.' . $ext . '.php')
829 . '" target="Documentation"><em>' . $ext
830 . '</em></a> extension is missing. Please check your PHP configuration.'
834 Core
::warnMissingExtension($ext, true, $extra);
835 $printed = ob_get_contents();
838 $this->assertGreaterThan(0, mb_strpos((string) $printed, $warn));
842 * Test for Core::signSqlQuery
844 public function testSignSqlQuery(): void
846 $_SESSION[' HMAC_secret '] = hash('sha1', 'test');
847 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
848 $signature = Core
::signSqlQuery($sqlQuery);
849 $hmac = '33371e8680a640dc05944a2a24e6e630d3e9e3dba24464135f2fb954c3a4ffe2';
850 $this->assertSame($hmac, $signature, 'The signature must match the computed one');
854 * Test for Core::checkSqlQuerySignature
856 public function testCheckSqlQuerySignature(): void
858 $_SESSION[' HMAC_secret '] = hash('sha1', 'test');
859 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
860 $hmac = '33371e8680a640dc05944a2a24e6e630d3e9e3dba24464135f2fb954c3a4ffe2';
861 $this->assertTrue(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
865 * Test for Core::checkSqlQuerySignature
867 public function testCheckSqlQuerySignatureFails(): void
869 $_SESSION[' HMAC_secret '] = hash('sha1', '132654987gguieunofz');
870 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
871 $hmac = '33371e8680a640dc05944a2a24e6e630d3e9e3dba24464135f2fb954c3a4ffe2';
872 $this->assertFalse(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
876 * Test for Core::checkSqlQuerySignature
878 public function testCheckSqlQuerySignatureFailsBadHash(): void
880 $_SESSION[' HMAC_secret '] = hash('sha1', 'test');
881 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
882 $hmac = '3333333380a640dc05944a2a24e6e630d3e9e3dba24464135f2fb954c3eeeeee';
883 $this->assertFalse(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
887 * Test for Core::checkSqlQuerySignature
889 public function testCheckSqlQuerySignatureFailsNoSession(): void
891 $_SESSION[' HMAC_secret '] = 'empty';
892 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
893 $hmac = '3333333380a640dc05944a2a24e6e630d3e9e3dba24464135f2fb954c3eeeeee';
894 $this->assertFalse(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
898 * Test for Core::checkSqlQuerySignature
900 public function testCheckSqlQuerySignatureFailsFromAnotherSession(): void
902 $_SESSION[' HMAC_secret '] = hash('sha1', 'firstSession');
903 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
904 $hmac = Core
::signSqlQuery($sqlQuery);
905 $this->assertTrue(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
906 $_SESSION[' HMAC_secret '] = hash('sha1', 'secondSession');
907 // Try to use the token (hmac) from the previous session
908 $this->assertFalse(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
912 * Test for Core::checkSqlQuerySignature
914 public function testCheckSqlQuerySignatureFailsBlowfishSecretChanged(): void
916 $GLOBALS['cfg']['blowfish_secret'] = '';
917 $_SESSION[' HMAC_secret '] = hash('sha1', 'firstSession');
918 $sqlQuery = 'SELECT * FROM `test`.`db` WHERE 1;';
919 $hmac = Core
::signSqlQuery($sqlQuery);
920 $this->assertTrue(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
921 $GLOBALS['cfg']['blowfish_secret'] = str_repeat('a', 32);
922 // Try to use the previous HMAC signature
923 $this->assertFalse(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
925 $GLOBALS['cfg']['blowfish_secret'] = str_repeat('a', 32);
926 // Generate the HMAC signature to check that it works
927 $hmac = Core
::signSqlQuery($sqlQuery);
928 // Must work now, (good secret and blowfish_secret)
929 $this->assertTrue(Core
::checkSqlQuerySignature($sqlQuery, $hmac));
932 public function testPopulateRequestWithEncryptedQueryParams(): void
935 $GLOBALS['config']->set('URLQueryEncryption', true);
936 $GLOBALS['config']->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
938 $_GET = ['pos' => '0', 'eq' => Url
::encryptQuery('{"db":"test_db","table":"test_table"}')];
941 $request = $this->createStub(ServerRequest
::class);
942 $request->method('getQueryParams')->willReturn($_GET);
943 $request->method('getParsedBody')->willReturn(null);
944 $request->method('withQueryParams')->willReturnSelf();
945 $request->method('withParsedBody')->willReturnSelf();
947 Core
::populateRequestWithEncryptedQueryParams($request);
949 $expected = ['pos' => '0', 'db' => 'test_db', 'table' => 'test_table'];
951 $this->assertEquals($expected, $_GET);
952 $this->assertEquals($expected, $_REQUEST);
956 * @param string[] $encrypted
957 * @param string[] $decrypted
959 * @dataProvider providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam
961 public function testPopulateRequestWithEncryptedQueryParamsWithInvalidParam(
966 $GLOBALS['config']->set('URLQueryEncryption', true);
967 $GLOBALS['config']->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
970 $_REQUEST = $encrypted;
972 $request = $this->createStub(ServerRequest
::class);
973 $request->method('getQueryParams')->willReturn($_GET);
974 $request->method('getParsedBody')->willReturn(null);
975 $request->method('withQueryParams')->willReturnSelf();
976 $request->method('withParsedBody')->willReturnSelf();
978 Core
::populateRequestWithEncryptedQueryParams($request);
980 $this->assertEquals($decrypted, $_GET);
981 $this->assertEquals($decrypted, $_REQUEST);
985 * @return array<int, array<int, array<string, string|mixed[]>>>
987 public function providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam(): array
993 [['eq' => 'invalid'], []],