diff --git a/.github/workflows/test-selenium.yml b/.github/workflows/test-selenium.yml index 7662b261fa..d54608dfa4 100644 --- a/.github/workflows/test-selenium.yml +++ b/.github/workflows/test-selenium.yml @@ -54,6 +54,8 @@ jobs: db-server: ["mysql:5.7"] steps: - uses: actions/checkout@v2 + - name: Refresh apt cache + run: sudo apt-get update - name: Install gettext run: sudo apt-get install -y gettext - name: Generate mo files diff --git a/ChangeLog b/ChangeLog index 33fac76130..9c47335afb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -121,6 +121,8 @@ phpMyAdmin - ChangeLog - issue #16920 Fixed "Uncaught TypeError: PhpMyAdmin\Import::detectType()" on ODS import - issue #16926 Fixed ODS import warning: Undefined array key "ods_empty_rows" - issue #16888 Fixed JS error on renaming tables in saved Designer page +- issue #16504 Fixed create view dialog is too big and won't scroll (on small screens) +- issue #16931 Fixed php notice "Undefined index: utf8mb3" on MySQL 8.0.11+ servers with default utf8 server charset 5.1.0 (2021-02-24) - issue #15350 Change Media (MIME) type references to Media type diff --git a/js/src/functions.js b/js/src/functions.js index 7daec25843..ed26558413 100644 --- a/js/src/functions.js +++ b/js/src/functions.js @@ -4565,6 +4565,7 @@ Functions.createViewDialog = function ($this) { var $dialog = $('
').attr('id', 'createViewDialog').append(data.message).dialog({ width: 600, minWidth: 400, + height: $(window).height(), modal: true, buttons: buttonOptions, title: Messages.strCreateView, diff --git a/libraries/classes/Charsets.php b/libraries/classes/Charsets.php index d9266061cf..cb2525f22f 100644 --- a/libraries/classes/Charsets.php +++ b/libraries/classes/Charsets.php @@ -159,7 +159,28 @@ class Charsets $serverCharset = $dbi->fetchValue('SELECT @@character_set_server;'); } - self::$serverCharset = self::$charsets[$serverCharset]; + self::$serverCharset = self::$charsets[$serverCharset] ?? null; + + // MySQL 8.0.11+ fallback, issue #16931 + if (self::$serverCharset === null && $serverCharset === 'utf8mb3') { + // See: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-charset + // The utf8mb3 character set will be replaced by utf8mb4 in a future MySQL version. + // The utf8 character set is currently an alias for utf8mb3, + // but will at that point become a reference to utf8mb4. + // To avoid ambiguity about the meaning of utf8, + // consider specifying utf8mb4 explicitly for character set references instead of utf8. + // Warning: #3719 'utf8' is currently an alias for the character set UTF8MB3 [...] + return self::$charsets['utf8']; + } + + if (self::$serverCharset === null) {// Fallback in case nothing is found + return Charset::fromServer( + [ + 'Charset' => __('Unknown'), + 'Description' => __('Unknown'), + ] + ); + } return self::$serverCharset; } diff --git a/test/classes/AbstractTestCase.php b/test/classes/AbstractTestCase.php index 7db47765e5..95cb2d22af 100644 --- a/test/classes/AbstractTestCase.php +++ b/test/classes/AbstractTestCase.php @@ -39,6 +39,20 @@ abstract class AbstractTestCase extends TestCase '__PHPUNIT_BOOTSTRAP', ]; + /** + * The DatabaseInterface loaded by setGlobalDbi + * + * @var DatabaseInterface + */ + protected $dbi; + + /** + * The DbiDummy loaded by setGlobalDbi + * + * @var DbiDummy + */ + protected $dummyDbi; + /** * Prepares environment for the test. * Clean all variables @@ -126,7 +140,9 @@ abstract class AbstractTestCase extends TestCase protected function setGlobalDbi(): void { global $dbi; - $dbi = DatabaseInterface::load(new DbiDummy()); + $this->dummyDbi = new DbiDummy(); + $this->dbi = DatabaseInterface::load($this->dummyDbi); + $dbi = $this->dbi; } protected function setGlobalConfig(): void diff --git a/test/classes/CharsetsTest.php b/test/classes/CharsetsTest.php index 2507da442c..086d2562f8 100644 --- a/test/classes/CharsetsTest.php +++ b/test/classes/CharsetsTest.php @@ -17,11 +17,65 @@ class CharsetsTest extends AbstractTestCase protected function setUp(): void { parent::setUp(); + parent::setGlobalDbi(); $GLOBALS['server'] = 0; $GLOBALS['cfg']['DBG']['sql'] = false; $GLOBALS['cfg']['Server']['DisableIS'] = false; } + public function testGetServerCharset(): void + { + $this->dummyDbi->addResult( + 'SHOW SESSION VARIABLES LIKE \'character_set_server\';', + [ + [ + 'character_set_server', + 'utf8mb3', + ], + ], + [ + 'Variable_name', + 'Value', + ] + ); + $this->dummyDbi->addResult( + 'SHOW SESSION VARIABLES LIKE \'character_set_server\';', + false + ); + $this->dummyDbi->addResult( + 'SELECT @@character_set_server;', + false + ); + $this->dummyDbi->addResult( + 'SHOW SESSION VARIABLES LIKE \'character_set_server\';', + false + ); + $this->dummyDbi->addResult( + 'SELECT @@character_set_server;', + [ + ['utf8mb3'], + ] + ); + + $charset = Charsets::getServerCharset( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['DisableIS'] + ); + $this->assertSame('utf8', $charset->getName()); + + $charset = Charsets::getServerCharset( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['DisableIS'] + ); + $this->assertSame('Unknown', $charset->getName()); + + $charset = Charsets::getServerCharset( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['DisableIS'] + ); + $this->assertSame('utf8', $charset->getName()); + } + public function testFindCollationByName(): void { $this->assertNull(Charsets::findCollationByName( diff --git a/test/classes/DatabaseInterfaceTest.php b/test/classes/DatabaseInterfaceTest.php index f1fb5a25d7..b11681c1b6 100644 --- a/test/classes/DatabaseInterfaceTest.php +++ b/test/classes/DatabaseInterfaceTest.php @@ -20,9 +20,6 @@ use stdClass; */ class DatabaseInterfaceTest extends AbstractTestCase { - /** @var DatabaseInterface */ - private $dbi; - /** * Configures test parameters. */ @@ -47,20 +44,19 @@ class DatabaseInterfaceTest extends AbstractTestCase public function testGetCurrentUser($value, string $string, array $expected): void { SessionCache::remove('mysql_cur_user'); + parent::setGlobalDbi(); - $extension = new DbiDummy(); - $extension->setResult('SELECT CURRENT_USER();', $value); - - $dbi = new DatabaseInterface($extension); + $this->dummyDbi->addResult('SELECT CURRENT_USER();', $value); + $this->dummyDbi->addResult('SELECT CURRENT_USER();', $value); $this->assertEquals( $expected, - $dbi->getCurrentUserAndHost() + $this->dbi->getCurrentUserAndHost() ); $this->assertEquals( $string, - $dbi->getCurrentUser() + $this->dbi->getCurrentUser() ); } @@ -277,15 +273,13 @@ class DatabaseInterfaceTest extends AbstractTestCase public function testIsAmazonRdsData(array $value, bool $expected): void { SessionCache::remove('is_amazon_rds'); + parent::setGlobalDbi(); - $extension = new DbiDummy(); - $extension->setResult('SELECT @@basedir', $value); - - $dbi = new DatabaseInterface($extension); + $this->dummyDbi->addResult('SELECT @@basedir', $value); $this->assertEquals( $expected, - $dbi->isAmazonRds() + $this->dbi->isAmazonRds() ); } diff --git a/test/classes/Stubs/DbiDummy.php b/test/classes/Stubs/DbiDummy.php index 3aa5ac03f9..51daa77c56 100644 --- a/test/classes/Stubs/DbiDummy.php +++ b/test/classes/Stubs/DbiDummy.php @@ -38,8 +38,34 @@ use const MYSQLI_TYPE_STRING; */ class DbiDummy implements DbiExtension { - /** @var array */ - private $queries = []; + /** + * First in, last out queries + * + * The results will be distributed in the filo way + * + * @var array + * @phpstan-var array{ + * 'query': string, + * 'result': ((int[]|string[]|array{string: string})[])|bool|empty-array, + * 'columns'?: string[], + * 'metadata'?: object[]|empty-array, + * 'used'?: bool, + * 'pos'?: int + * }[] + */ + private $filoQueries = []; + + /** + * @var array + * @phpstan-var array{ + * 'query': string, + * 'result': ((int[]|string[]|array{string: string})[])|bool|empty-array, + * 'columns'?: string[], + * 'metadata'?: object[]|empty-array, + * 'pos'?: int + * }[] + */ + private $dummyQueries = []; public const OFFSET_GLOBAL = 1000; @@ -80,6 +106,33 @@ class DbiDummy implements DbiExtension return true; } + /** + * @return false|int|null + */ + private function findFiloQuery(string $query) + { + for ($i = 0, $nb = count($this->filoQueries); $i < $nb; $i++) { + if ($this->filoQueries[$i]['query'] !== $query) { + continue; + } + + if ($this->filoQueries[$i]['used'] ?? false) { + continue;// Is has already been used + } + + $this->filoQueries[$i]['pos'] = 0; + $this->filoQueries[$i]['used'] = true; + + if (! is_array($this->filoQueries[$i]['result'])) { + return false; + } + + return $i; + } + + return null; + } + /** * runs a query and returns the result * @@ -92,26 +145,18 @@ class DbiDummy implements DbiExtension public function realQuery($query, $link = null, $options = 0) { $query = trim((string) preg_replace('/ */', ' ', str_replace("\n", ' ', $query))); - for ($i = 0, $nb = count($this->queries); $i < $nb; $i++) { - if ($this->queries[$i]['query'] != $query) { - continue; - } - - $this->queries[$i]['pos'] = 0; - if (! is_array($this->queries[$i]['result'])) { - return false; - } - - return $i; + $filoQuery = $this->findFiloQuery($query); + if ($filoQuery !== null) {// Found a matching query + return $filoQuery; } - for ($i = 0, $nb = count($GLOBALS['dummy_queries']); $i < $nb; $i++) { - if ($GLOBALS['dummy_queries'][$i]['query'] != $query) { + for ($i = 0, $nb = count($this->dummyQueries); $i < $nb; $i++) { + if ($this->dummyQueries[$i]['query'] !== $query) { continue; } - $GLOBALS['dummy_queries'][$i]['pos'] = 0; - if (! is_array($GLOBALS['dummy_queries'][$i]['result'])) { + $this->dummyQueries[$i]['pos'] = 0; + if (! is_array($this->dummyQueries[$i]['result'])) { return false; } @@ -437,16 +482,21 @@ class DbiDummy implements DbiExtension /** * Adds query result for testing * - * @param string $query SQL - * @param array|false $result Expected result + * @param string $query SQL + * @param array|bool $result Expected result + * @param string[] $columns The result columns + * @param object[] $metadata The result metadata + * @phpstan-param (int[]|string[]|array{string: string})[]|bool $result * * @return void */ - public function setResult($query, $result) + public function addResult(string $query, $result, array $columns = [], array $metadata = []) { - $this->queries[] = [ + $this->filoQueries[] = [ 'query' => $query, 'result' => $result, + 'columns' => $columns, + 'metadata' => $metadata, ]; } @@ -476,10 +526,10 @@ class DbiDummy implements DbiExtension } if ($result >= self::OFFSET_GLOBAL) { - return $GLOBALS['dummy_queries'][$result - self::OFFSET_GLOBAL]; + return $this->dummyQueries[$result - self::OFFSET_GLOBAL]; } - return $this->queries[$result]; + return $this->filoQueries[$result]; } private function init(): void @@ -487,7 +537,7 @@ class DbiDummy implements DbiExtension /** * Array of queries this "driver" supports */ - $GLOBALS['dummy_queries'] = [ + $this->dummyQueries = [ [ 'query' => 'SELECT 1', 'result' => [['1']], @@ -817,12 +867,24 @@ class DbiDummy implements DbiExtension 'Maxlen', ], 'result' => [ + [ + 'armscii8', + 'ARMSCII-8 Armenian', + 'armscii8_general_ci', + '1', + ], [ 'utf8', 'utf8_general_ci', 'UTF-8 Unicode', '3', ], + [ + 'utf8mb4', + 'UTF-8 Unicode', + 'utf8mb4_0900_ai_ci', + '4', + ], [ 'latin1', 'latin1_swedish_ci', @@ -848,6 +910,22 @@ class DbiDummy implements DbiExtension 'Sortlen', ], 'result' => [ + [ + 'utf8mb4_general_ci', + 'utf8mb4', + '45', + 'Yes', + 'Yes', + '1', + ], + [ + 'armscii8_general_ci', + 'armscii8', + '32', + 'Yes', + 'Yes', + '1', + ], [ 'utf8_general_ci', 'utf8',