diff --git a/libraries/classes/Charsets.php b/libraries/classes/Charsets.php index dedf326603..f6d3e5f5f6 100644 --- a/libraries/classes/Charsets.php +++ b/libraries/classes/Charsets.php @@ -9,8 +9,8 @@ declare(strict_types=1); namespace PhpMyAdmin; -use PhpMyAdmin\DatabaseInterface; -use PhpMyAdmin\Util; +use PhpMyAdmin\Charsets\Charset; +use PhpMyAdmin\Charsets\Collation; /** * Class used to manage MySQL charsets @@ -24,7 +24,7 @@ class Charsets * * @var array */ - public static $mysql_charset_map = [ + public static $mysqlCharsetMap = [ 'big5' => 'big5', 'cp-866' => 'cp866', 'euc-jp' => 'ujis', @@ -50,20 +50,24 @@ class Charsets 'windows-1257' => 'cp1257', ]; - private static $_charsets = []; - /** * The charset for the server - * - * @var string + * @var Charset|null */ - private static $_charset_server = null; - private static $_charsets_descriptions = []; - private static $_collations = []; - private static $_default_collations = []; + private static $serverCharset = null; /** - * Loads charset data from the MySQL server. + * @var array + */ + private static $charsets = []; + + /** + * @var array> + */ + private static $collations = []; + + /** + * Loads charset data from the server * * @param DatabaseInterface $dbi DatabaseInterface instance * @param boolean $disableIs Disable use of INFORMATION_SCHEMA @@ -73,7 +77,7 @@ class Charsets private static function loadCharsets(DatabaseInterface $dbi, bool $disableIs): void { /* Data already loaded */ - if (count(self::$_charsets) > 0) { + if (count(self::$charsets) > 0) { return; } @@ -81,24 +85,24 @@ class Charsets $sql = 'SHOW CHARACTER SET'; } else { $sql = 'SELECT `CHARACTER_SET_NAME` AS `Charset`,' - . ' `DESCRIPTION` AS `Description`' + . ' `DEFAULT_COLLATE_NAME` AS `Default collation`,' + . ' `DESCRIPTION` AS `Description`,' + . ' `MAXLEN` AS `Maxlen`' . ' FROM `information_schema`.`CHARACTER_SETS`'; } $res = $dbi->query($sql); - self::$_charsets = []; + self::$charsets = []; while ($row = $dbi->fetchAssoc($res)) { - $name = $row['Charset']; - self::$_charsets[] = $name; - self::$_charsets_descriptions[$name] = $row['Description']; + self::$charsets[$row['Charset']] = Charset::fromServer($row); } $dbi->freeResult($res); - sort(self::$_charsets, SORT_STRING); + ksort(self::$charsets, SORT_STRING); } /** - * Loads collation data from the MySQL server. + * Loads collation data from the server * * @param DatabaseInterface $dbi DatabaseInterface instance * @param boolean $disableIs Disable use of INFORMATION_SCHEMA @@ -108,105 +112,79 @@ class Charsets private static function loadCollations(DatabaseInterface $dbi, bool $disableIs): void { /* Data already loaded */ - if (count(self::$_collations) > 0) { + if (count(self::$collations) > 0) { return; } if ($disableIs) { $sql = 'SHOW COLLATION'; } else { - $sql = 'SELECT `CHARACTER_SET_NAME` AS `Charset`,' - . ' `COLLATION_NAME` AS `Collation`, `IS_DEFAULT` AS `Default`' + $sql = 'SELECT `COLLATION_NAME` AS `Collation`,' + . ' `CHARACTER_SET_NAME` AS `Charset`,' + . ' `ID` AS `Id`,' + . ' `IS_DEFAULT` AS `Default`,' + . ' `IS_COMPILED` AS `Compiled`,' + . ' `SORTLEN` AS `Sortlen`' . ' FROM `information_schema`.`COLLATIONS`'; } - $res = $dbi->query($sql); + + self::$collations = []; while ($row = $dbi->fetchAssoc($res)) { - $char_set_name = $row['Charset']; - $name = $row['Collation']; - self::$_collations[$char_set_name][] = $name; - if ($row['Default'] == 'Yes' || $row['Default'] == '1') { - self::$_default_collations[$char_set_name] = $name; - } + self::$collations[$row['Charset']][$row['Collation']] = Collation::fromServer($row); } $dbi->freeResult($res); - foreach (self::$_collations as $key => $value) { - sort(self::$_collations[$key], SORT_STRING); + foreach (array_keys(self::$collations) as $charset) { + ksort(self::$collations[$charset], SORT_STRING); } } /** - * Get current MySQL server charset. + * Get current server charset * - * @param DatabaseInterface $dbi DatabaseInterface instance + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param boolean $disableIs Disable use of INFORMATION_SCHEMA * - * @return string + * @return Charset */ - public static function getServerCharset(DatabaseInterface $dbi): string + public static function getServerCharset(DatabaseInterface $dbi, bool $disableIs): Charset { - if (self::$_charset_server !== null) { - return self::$_charset_server; - } else { - self::$_charset_server = $dbi->getVariable('character_set_server'); - return self::$_charset_server; + if (self::$serverCharset !== null) { + return self::$serverCharset; } + self::loadCharsets($dbi, $disableIs); + $serverCharset = $dbi->getVariable('character_set_server'); + self::$serverCharset = self::$charsets[$serverCharset]; + return self::$serverCharset; } /** - * Get MySQL charsets + * Get all server charsets * * @param DatabaseInterface $dbi DatabaseInterface instance * @param boolean $disableIs Disable use of INFORMATION_SCHEMA * * @return array */ - public static function getMySQLCharsets(DatabaseInterface $dbi, bool $disableIs): array + public static function getCharsets(DatabaseInterface $dbi, bool $disableIs): array { self::loadCharsets($dbi, $disableIs); - return self::$_charsets; + return self::$charsets; } /** - * Get MySQL charsets descriptions + * Get all server collations * * @param DatabaseInterface $dbi DatabaseInterface instance * @param boolean $disableIs Disable use of INFORMATION_SCHEMA * * @return array */ - public static function getMySQLCharsetsDescriptions(DatabaseInterface $dbi, bool $disableIs): array - { - self::loadCharsets($dbi, $disableIs); - return self::$_charsets_descriptions; - } - - /** - * Get MySQL collations - * - * @param DatabaseInterface $dbi DatabaseInterface instance - * @param boolean $disableIs Disable use of INFORMATION_SCHEMA - * - * @return array - */ - public static function getMySQLCollations(DatabaseInterface $dbi, bool $disableIs): array + public static function getCollations(DatabaseInterface $dbi, bool $disableIs): array { self::loadCollations($dbi, $disableIs); - return self::$_collations; - } - - /** - * Get MySQL default collations - * - * @param DatabaseInterface $dbi DatabaseInterface instance - * @param boolean $disableIs Disable use of INFORMATION_SCHEMA - * - * @return array - */ - public static function getMySQLCollationsDefault(DatabaseInterface $dbi, bool $disableIs): array - { - self::loadCollations($dbi, $disableIs); - return self::$_default_collations; + return self::$collations; } /** @@ -232,34 +210,25 @@ class Charsets bool $submitOnChange = false ): string { self::loadCharsets($dbi, $disableIs); - if (empty($name)) { - $name = 'character_set'; + + $charsets = []; + /** @var Charset $charset */ + foreach (self::$charsets as $charset) { + $charsets[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'is_selected' => $default === $charset->getName(), + ]; } - $return_str = '' . "\n"; - - return $return_str; + $template = new Template(); + return $template->render('charset_select', [ + 'name' => $name, + 'id' => $id, + 'submit_on_change' => $submitOnChange, + 'has_label' => $label, + 'charsets' => $charsets, + ]); } /** @@ -286,40 +255,34 @@ class Charsets ): string { self::loadCharsets($dbi, $disableIs); self::loadCollations($dbi, $disableIs); - if (empty($name)) { - $name = 'collation'; - } - $return_str = '' . "\n"; - return $return_str; + $template = new Template(); + return $template->render('collation_select', [ + 'name' => $name, + 'id' => $id, + 'submit_on_change' => $submitOnChange, + 'has_label' => $label, + 'charsets' => $charsets, + ]); } /** diff --git a/libraries/classes/Charsets/Charset.php b/libraries/classes/Charsets/Charset.php new file mode 100644 index 0000000000..4395462b92 --- /dev/null +++ b/libraries/classes/Charsets/Charset.php @@ -0,0 +1,103 @@ +name = $name; + $this->description = $description; + $this->defaultCollation = $defaultCollation; + $this->maxLength = $maxLength; + } + + /** + * @param array $state State obtained from the database server + * @return Charset + */ + public static function fromServer(array $state): self + { + return new self( + $state['Charset'] ?? '', + $state['Description'] ?? '', + $state['Default collation'] ?? '', + (int) $state['Maxlen'] ?? 0 + ); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @return string + */ + public function getDefaultCollation(): string + { + return $this->defaultCollation; + } + + /** + * @return int + */ + public function getMaxLength(): int + { + return $this->maxLength; + } +} diff --git a/libraries/classes/Charsets/Collation.php b/libraries/classes/Charsets/Collation.php new file mode 100644 index 0000000000..6a227dff82 --- /dev/null +++ b/libraries/classes/Charsets/Collation.php @@ -0,0 +1,174 @@ +name = $name; + $this->charset = $charset; + $this->id = $id; + $this->isDefault = $isDefault; + $this->isCompiled = $isCompiled; + $this->sortLength = $sortLength; + $this->padAttribute = $padAttribute; + $this->description = Charsets::getCollationDescr($this->name); + } + + /** + * @param array $state State obtained from the database server + * @return self + */ + public static function fromServer(array $state): self + { + return new self( + $state['Collation'] ?? '', + $state['Charset'] ?? '', + (int) $state['Id'] ?? 0, + isset($state['Default']) && ($state['Default'] === 'Yes' || $state['Default'] === '1'), + isset($state['Compiled']) && ($state['Compiled'] === 'Yes' || $state['Compiled'] === '1'), + (int) $state['Sortlen'] ?? 0, + $state['Pad_attribute'] ?? '' + ); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @return string + */ + public function getCharset(): string + { + return $this->charset; + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @return bool + */ + public function isDefault(): bool + { + return $this->isDefault; + } + + /** + * @return bool + */ + public function isCompiled(): bool + { + return $this->isCompiled; + } + + /** + * @return int + */ + public function getSortLength(): int + { + return $this->sortLength; + } + + /** + * @return string + */ + public function getPadAttribute(): string + { + return $this->padAttribute; + } +} diff --git a/libraries/classes/Controllers/HomeController.php b/libraries/classes/Controllers/HomeController.php index 17a9a1b0bf..b45389f28f 100644 --- a/libraries/classes/Controllers/HomeController.php +++ b/libraries/classes/Controllers/HomeController.php @@ -167,12 +167,7 @@ class HomeController extends AbstractController $hostInfo .= ')'; } - $unicode = Charsets::$mysql_charset_map['utf-8']; - $charsets = Charsets::getMySQLCharsetsDescriptions( - $this->dbi, - $cfg['Server']['DisableIS'] - ); - + $serverCharset = Charsets::getServerCharset($this->dbi, $cfg['Server']['DisableIS']); $databaseServer = [ 'host' => $hostInfo, 'type' => Util::getServerType(), @@ -180,7 +175,7 @@ class HomeController extends AbstractController 'version' => $this->dbi->getVersionString() . ' - ' . $this->dbi->getVersionComment(), 'protocol' => $this->dbi->getProtoInfo(), 'user' => $this->dbi->fetchValue('SELECT USER();'), - 'charset' => $charsets[$unicode] . ' (' . $unicode . ')', + 'charset' => $serverCharset->getDescription() . ' (' . $serverCharset->getName() . ')', ]; } diff --git a/libraries/classes/Controllers/Server/CollationsController.php b/libraries/classes/Controllers/Server/CollationsController.php index 13e28d1114..2d806e85fb 100644 --- a/libraries/classes/Controllers/Server/CollationsController.php +++ b/libraries/classes/Controllers/Server/CollationsController.php @@ -10,6 +10,8 @@ declare(strict_types=1); namespace PhpMyAdmin\Controllers\Server; use PhpMyAdmin\Charsets; +use PhpMyAdmin\Charsets\Charset; +use PhpMyAdmin\Charsets\Collation; use PhpMyAdmin\Controllers\AbstractController; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Response; @@ -27,58 +29,36 @@ class CollationsController extends AbstractController */ private $charsets; - /** - * @var array|null - */ - private $charsetsDescriptions; - /** * @var array|null */ private $collations; - /** - * @var array|null - */ - private $defaultCollations; - /** * CollationsController constructor. * - * @param Response $response Response object - * @param DatabaseInterface $dbi DatabaseInterface object - * @param Template $template Template object - * @param array|null $charsets Array of charsets - * @param array|null $charsetsDescriptions Array of charsets descriptions - * @param array|null $collations Array of collations - * @param array|null $defaultCollations Array of default collations + * @param Response $response Response object + * @param DatabaseInterface $dbi DatabaseInterface object + * @param Template $template Template object + * @param array|null $charsets Array of charsets + * @param array|null $collations Array of collations */ public function __construct( $response, $dbi, Template $template, ?array $charsets = null, - ?array $charsetsDescriptions = null, - ?array $collations = null, - ?array $defaultCollations = null + ?array $collations = null ) { global $cfg; parent::__construct($response, $dbi, $template); - $this->charsets = $charsets ?? Charsets::getMySQLCharsets( + $this->charsets = $charsets ?? Charsets::getCharsets( $this->dbi, $cfg['Server']['DisableIS'] ); - $this->charsetsDescriptions = $charsetsDescriptions ?? Charsets::getMySQLCharsetsDescriptions( - $this->dbi, - $cfg['Server']['DisableIS'] - ); - $this->collations = $collations ?? Charsets::getMySQLCollations( - $this->dbi, - $cfg['Server']['DisableIS'] - ); - $this->defaultCollations = $defaultCollations ?? Charsets::getMySQLCollationsDefault( + $this->collations = $collations ?? Charsets::getCollations( $this->dbi, $cfg['Server']['DisableIS'] ); @@ -94,19 +74,21 @@ class CollationsController extends AbstractController include_once ROOT_PATH . 'libraries/server_common.inc.php'; $charsets = []; + /** @var Charset $charset */ foreach ($this->charsets as $charset) { $charsetCollations = []; - foreach ($this->collations[$charset] as $collation) { + /** @var Collation $collation */ + foreach ($this->collations[$charset->getName()] as $collation) { $charsetCollations[] = [ - 'name' => $collation, - 'description' => Charsets::getCollationDescr($collation), - 'is_default' => $collation === $this->defaultCollations[$charset], + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_default' => $collation->isDefault(), ]; } $charsets[] = [ - 'name' => $charset, - 'description' => $this->charsetsDescriptions[$charset] ?? '', + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), 'collations' => $charsetCollations, ]; } diff --git a/libraries/classes/Controllers/Server/DatabasesController.php b/libraries/classes/Controllers/Server/DatabasesController.php index b12ce09409..72206db8cc 100644 --- a/libraries/classes/Controllers/Server/DatabasesController.php +++ b/libraries/classes/Controllers/Server/DatabasesController.php @@ -157,16 +157,16 @@ class DatabasesController extends AbstractController $sqlQuery = 'CREATE DATABASE ' . Util::backquote($params['new_db']); if (! empty($params['db_collation'])) { list($databaseCharset) = explode('_', $params['db_collation']); - $charsets = Charsets::getMySQLCharsets( + $charsets = Charsets::getCharsets( $this->dbi, $cfg['Server']['DisableIS'] ); - $collations = Charsets::getMySQLCollations( + $collations = Charsets::getCollations( $this->dbi, $cfg['Server']['DisableIS'] ); - if (in_array($databaseCharset, $charsets) - && in_array($params['db_collation'], $collations[$databaseCharset]) + if (in_array($databaseCharset, array_keys($charsets)) + && in_array($params['db_collation'], array_keys($collations[$databaseCharset])) ) { $sqlQuery .= ' DEFAULT' . Util::getCharsetQueryPart($params['db_collation']); diff --git a/libraries/classes/Dbi/DbiDummy.php b/libraries/classes/Dbi/DbiDummy.php index f0bb175791..c36936427f 100644 --- a/libraries/classes/Dbi/DbiDummy.php +++ b/libraries/classes/Dbi/DbiDummy.php @@ -781,47 +781,71 @@ class DbiDummy implements DbiExtension ], [ 'query' => 'SELECT `CHARACTER_SET_NAME` AS `Charset`,' - . ' `DESCRIPTION` AS `Description`' + . ' `DEFAULT_COLLATE_NAME` AS `Default collation`,' + . ' `DESCRIPTION` AS `Description`,' + . ' `MAXLEN` AS `Maxlen`' . ' FROM `information_schema`.`CHARACTER_SETS`', 'columns' => [ 'Charset', + 'Default collation', 'Description', - ], - 'result' => [ - [ - 'utf8', - 'UTF-8 Unicode', - ], - [ - 'latin1', - 'cp1252 West European', - ], - ], - ], - [ - 'query' => 'SELECT `CHARACTER_SET_NAME` AS `Charset`,' - . ' `COLLATION_NAME` AS `Collation`, `IS_DEFAULT` AS `Default`' - . ' FROM `information_schema`.`COLLATIONS`', - 'columns' => [ - 'Charset', - 'Collation', - 'Default', + 'Maxlen', ], 'result' => [ [ 'utf8', 'utf8_general_ci', - 'Yes', - ], - [ - 'utf8', - 'utf8_bin', - '', + 'UTF-8 Unicode', + '3', ], [ 'latin1', 'latin1_swedish_ci', + 'cp1252 West European', + '1', + ], + ], + ], + [ + 'query' => 'SELECT `COLLATION_NAME` AS `Collation`,' + . ' `CHARACTER_SET_NAME` AS `Charset`,' + . ' `ID` AS `Id`,' + . ' `IS_DEFAULT` AS `Default`,' + . ' `IS_COMPILED` AS `Compiled`,' + . ' `SORTLEN` AS `Sortlen`' + . ' FROM `information_schema`.`COLLATIONS`', + 'columns' => [ + 'Collation', + 'Charset', + 'Id', + 'Default', + 'Compiled', + 'Sortlen', + ], + 'result' => [ + [ + 'utf8_general_ci', + 'utf8', + '33', 'Yes', + 'Yes', + '1', + ], + [ + 'utf8_bin', + 'utf8', + '83', + '', + 'Yes', + '1', + ], + [ + 'latin1_swedish_ci', + 'latin1', + '8', + 'Yes', + 'Yes', + '1', ], ], ], diff --git a/libraries/classes/Plugins/Export/ExportSql.php b/libraries/classes/Plugins/Export/ExportSql.php index 957be411ae..d07758f9ec 100644 --- a/libraries/classes/Plugins/Export/ExportSql.php +++ b/libraries/classes/Plugins/Export/ExportSql.php @@ -775,13 +775,13 @@ class ExportSql extends ExportPlugin // so that a utility like the mysql client can interpret // the file correctly if (isset($GLOBALS['charset']) - && isset(Charsets::$mysql_charset_map[$GLOBALS['charset']]) + && isset(Charsets::$mysqlCharsetMap[$GLOBALS['charset']]) ) { // we got a charset from the export dialog - $set_names = Charsets::$mysql_charset_map[$GLOBALS['charset']]; + $set_names = Charsets::$mysqlCharsetMap[$GLOBALS['charset']]; } else { // by default we use the connection charset - $set_names = Charsets::$mysql_charset_map['utf-8']; + $set_names = Charsets::$mysqlCharsetMap['utf-8']; } if ($set_names == 'utf8' && $GLOBALS['dbi']->getVersion() > 50503) { $set_names = 'utf8mb4'; diff --git a/templates/charset_select.twig b/templates/charset_select.twig new file mode 100644 index 0000000000..afacd1da8d --- /dev/null +++ b/templates/charset_select.twig @@ -0,0 +1,13 @@ + diff --git a/templates/collation_select.twig b/templates/collation_select.twig new file mode 100644 index 0000000000..cc8b505a45 --- /dev/null +++ b/templates/collation_select.twig @@ -0,0 +1,17 @@ + diff --git a/test/classes/CharsetsTest.php b/test/classes/CharsetsTest.php index fb72887b02..9ded910e5e 100644 --- a/test/classes/CharsetsTest.php +++ b/test/classes/CharsetsTest.php @@ -459,6 +459,6 @@ class CharsetsTest extends TestCase $this->assertStringNotContainsString('Charset', $result); $this->assertStringContainsString('class="autosubmit"', $result); $this->assertStringContainsString('id="test_id"', $result); - $this->assertStringContainsString('selected="selected">latin1', $result); + $this->assertStringContainsString('selected>latin1', $result); } } diff --git a/test/classes/Controllers/Server/CollationsControllerTest.php b/test/classes/Controllers/Server/CollationsControllerTest.php index 557c811207..77356002bb 100644 --- a/test/classes/Controllers/Server/CollationsControllerTest.php +++ b/test/classes/Controllers/Server/CollationsControllerTest.php @@ -45,39 +45,10 @@ class CollationsControllerTest extends TestCase */ public function testIndexAction(): void { - $charsets = [ - 'armscii8', - 'ascii', - 'big5', - 'binary', - ]; - $charsetsDescriptions = [ - 'armscii8' => 'PMA_armscii8_general_ci', - 'ascii' => 'PMA_ascii_general_ci', - 'big5' => 'PMA_big5_general_ci', - 'binary' => 'PMA_binary_general_ci', - ]; - $collations = [ - 'armscii8' => ['armscii8'], - 'ascii' => ['ascii'], - 'big5' => ['big5'], - 'binary' => ['binary'], - ]; - $defaultCollations = [ - 'armscii8' => 'armscii8', - 'ascii' => 'ascii', - 'big5' => 'big5', - 'binary' => 'binary', - ]; - $controller = new CollationsController( Response::getInstance(), $GLOBALS['dbi'], - new Template(), - $charsets, - $charsetsDescriptions, - $collations, - $defaultCollations + new Template() ); $actual = $controller->indexAction(); @@ -95,31 +66,27 @@ class CollationsControllerTest extends TestCase $actual ); $this->assertStringContainsString( - 'PMA_armscii8_general_ci', + 'UTF-8 Unicode', $actual ); $this->assertStringContainsString( - 'armscii8', + 'utf8_general_ci', $actual ); $this->assertStringContainsString( - '' . Charsets::getCollationDescr('armscii8') . '', + '' . Charsets::getCollationDescr('utf8_general_ci') . '', $actual ); $this->assertStringContainsString( - 'PMA_ascii_general_ci', + 'cp1252 West European', $actual ); $this->assertStringContainsString( - 'ascii', + 'latin1_swedish_ci', $actual ); $this->assertStringContainsString( - 'PMA_big5_general_ci', - $actual - ); - $this->assertStringContainsString( - 'big5', + 'Swedish, case-insensitive', $actual ); }