Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG-5.12.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

- Added support for JPEG XL (`.jxl`) images, when ImageMagick supports it. JPEG XL images can now be uploaded, transformed, and set as a transform’s output format.
- BMP images can now be transformed and edited in the Image Editor.
- MySQL database backups and restores now carry over SSL/TLS settings from the system’s MySQL option files (e.g. `ssl-ca` or `disable-ssl-verify-server-cert`), if the database connection doesn’t have any `PDO::MYSQL_ATTR_SSL_*` attributes set. ([#19366](https://github.com/craftcms/cms/pull/19366))
- MySQL database backups and restores now respect the `PDO::MYSQL_ATTR_SSL_CAPATH` and `PDO::MYSQL_ATTR_SSL_CIPHER` database connection attributes. ([#19366](https://github.com/craftcms/cms/pull/19366))
- Craft can now determine the dimensions of BMP and JPEG XL images on remote volumes without downloading them.
- Removed support for the non-standard `ED256` passkey algorithm. ([#19701](https://github.com/craftcms/cms/pull/19701))
- Updated Axios to 1.20.0.
Expand Down
121 changes: 110 additions & 11 deletions src/db/mysql/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public function createColumnSchemaBuilder($type, $length = null): ColumnSchemaBu
public function getDefaultBackupCommand(?array $ignoreTables = null): string
{
$baseCommand = (new ShellCommand('mysqldump'))
->addArg('--defaults-file=', $this->_createDumpConfigFile())
->addArg('--defaults-file=', $this->_createDumpConfigFile('mysqldump'))
->addArg('--add-drop-table')
->addArg('--comments')
->addArg('--create-options')
Expand Down Expand Up @@ -266,7 +266,7 @@ public function getDefaultRestoreCommand(): string
{
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->restoreCommand;
$command = (new ShellCommand('mysql'))
->addArg('--defaults-file=', $this->_createDumpConfigFile())
->addArg('--defaults-file=', $this->_createDumpConfigFile('mysql'))
->addArg('{database}');

if ($commandFromConfig instanceof \Closure) {
Expand Down Expand Up @@ -455,10 +455,11 @@ protected function supportsColumnStatistics(): bool
/**
* Creates a temporary my.cnf file based on the DB config settings.
*
* @param string $program The program that the my.cnf file will be used with (`mysqldump` or `mysql`)
* @return string The path to the my.cnf file
* @throws ErrorException
*/
private function _createDumpConfigFile(): string
private function _createDumpConfigFile(string $program): string
{
$this->tempMyCnfPath = FileHelper::normalizePath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . StringHelper::randomString(12) . '.cnf';

Expand All @@ -477,14 +478,33 @@ private function _createDumpConfigFile(): string
}

// Certificates
if (isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CA])) {
$contents .= PHP_EOL . 'ssl_ca=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_CA];
}
if (isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CERT])) {
$contents .= PHP_EOL . 'ssl_cert=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_CERT];
}
if (isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_KEY])) {
$contents .= PHP_EOL . 'ssl_key=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_KEY];
if (
isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CA]) ||
(defined('PDO::MYSQL_ATTR_SSL_CAPATH') && isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CAPATH])) ||
isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CERT]) ||
isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_KEY]) ||
(defined('PDO::MYSQL_ATTR_SSL_CIPHER') && isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CIPHER]))
) {
if (isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CA])) {
$contents .= PHP_EOL . 'ssl_ca=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_CA];
}
if (defined('PDO::MYSQL_ATTR_SSL_CAPATH') && isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CAPATH])) {
$contents .= PHP_EOL . 'ssl_capath=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_CAPATH];
}
if (isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CERT])) {
$contents .= PHP_EOL . 'ssl_cert=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_CERT];
}
if (isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_KEY])) {
$contents .= PHP_EOL . 'ssl_key=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_KEY];
}
if (defined('PDO::MYSQL_ATTR_SSL_CIPHER') && isset($this->db->attributes[PDO::MYSQL_ATTR_SSL_CIPHER])) {
$contents .= PHP_EOL . 'ssl_cipher=' . $this->db->attributes[PDO::MYSQL_ATTR_SSL_CIPHER];
}
} else {
// The db connection wasn't explicitly configured with SSL attributes, but the normal
// my.cnf file chain (which --defaults-file causes mysqldump/mysql to ignore) might
// still specify them, so carry those over.
$contents .= $this->_sslDefaultsFromConfigFiles($program);
}

FileHelper::writeToFile($this->tempMyCnfPath, '');
Expand All @@ -495,6 +515,85 @@ private function _createDumpConfigFile(): string
return $this->tempMyCnfPath;
}

/**
* Returns SSL/TLS my.cnf directives based on whatever the normal MySQL config file chain
* (e.g. `/etc/my.cnf`, `~/.my.cnf`) would otherwise provide, since those files are ignored
* once `--defaults-file` is passed to `mysqldump`/`mysql`.
*
* @param string $program The program whose defaults should be checked (`mysqldump` or `mysql`)
* @return string
*/
private function _sslDefaultsFromConfigFiles(string $program): string
{
$shellCommand = (new ShellCommand($program))->addArg('--print-defaults');

// If we don't have proc_open, maybe we've got exec
if (!function_exists('proc_open') && function_exists('exec')) {
$shellCommand->useExec = true;
}

if (!$shellCommand->execute()) {
return '';
}

return $this->_sslDirectivesFromPrintDefaults($shellCommand->getOutput() ?? '');
}

/**
* Returns SSL/TLS my.cnf directives from the given `--print-defaults` output.
*
* @param string $output
* @return string
*/
private function _sslDirectivesFromPrintDefaults(string $output): string
{
// The output looks like:
//
// mysqldump would have been started with the following arguments:
// --ssl-ca=/path/to/ca.pem --ssl-verify-server-cert=0 --skip-ssl ...
//
// Values aren't quoted, so split on whitespace that is followed by `--` (rather than on all whitespace),
// so values containing spaces stay intact.
$pos = strpos($output, '--');
if ($pos === false) {
return '';
}

$tokens = preg_split('/\s+(?=--)/', trim(substr($output, $pos)), -1, PREG_SPLIT_NO_EMPTY);

$contents = '';

foreach ($tokens as $token) {
if (!preg_match('/^--([\w-]+)(?:=(.*))?$/s', trim($token), $match)) {
continue;
}

// Option names can use dashes or underscores, and can be prefixed with modifiers like `skip-` and `loose-`
$name = strtolower(str_replace('_', '-', $match[1]));
$baseName = preg_replace('/^(?:loose-)?(?:skip-|disable-|enable-)?/', '', $name);
if (!preg_match('/^(ssl|tls)(-|$)/', $baseName)) {
continue;
}

if (!isset($match[2])) {
$contents .= PHP_EOL . $name;
continue;
}

$value = trim($match[2], "\"'");

// Ignore redacted values
if (preg_match('/^\*+$/', $value)) {
continue;
}

// Backslashes are escape characters in option files (e.g. within Windows paths)
$contents .= PHP_EOL . $name . '=' . str_replace('\\', '\\\\', $value);
}
Comment thread
Copilot marked this conversation as resolved.

return $contents;
}

/**
* Returns the row format for the given table, if known.
*
Expand Down
78 changes: 78 additions & 0 deletions tests/unit/db/mysql/SchemaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace crafttests\unit\db\mysql;

use Craft;
use craft\db\mysql\Schema;
use craft\test\TestCase;

/**
* Unit tests for the MySQL Schema class.
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 5.12.0
*/
class SchemaTest extends TestCase
{
/**
* @dataProvider sslDirectivesFromPrintDefaultsDataProvider
* @param string $expected
* @param string $output
*/
public function testSslDirectivesFromPrintDefaults(string $expected, string $output): void
{
$schema = Craft::$app->getDb()->getSchema();
if (!$schema instanceof Schema) {
$this->markTestSkipped('The test database isn’t MySQL.');
}

self::assertSame($expected, $this->invokeMethod($schema, '_sslDirectivesFromPrintDefaults', [$output]));
}

public static function sslDirectivesFromPrintDefaultsDataProvider(): array
{
$header = 'mysqldump would have been started with the following arguments:' . PHP_EOL;
$nl = PHP_EOL;

return [
'empty' => ['', ''],
'header only' => ['', $header],
'no ssl options' => ['', $header . '--host=localhost --user=root --password=***** --quick'],
'basic' => [
"{$nl}ssl-ca=/etc/ssl/ca.pem{$nl}ssl-cert=/etc/ssl/cert.pem{$nl}ssl-key=/etc/ssl/key.pem",
$header . '--host=db --ssl-ca=/etc/ssl/ca.pem --ssl-cert=/etc/ssl/cert.pem --ssl-key=/etc/ssl/key.pem --quick',
],
'flags and prefixes' => [
"{$nl}skip-ssl{$nl}disable-ssl-verify-server-cert{$nl}loose-ssl-mode=REQUIRED{$nl}ssl=0",
$header . '--skip-ssl --disable-ssl-verify-server-cert --loose-ssl-mode=REQUIRED --ssl=0',
],
'verification and tls' => [
"{$nl}skip-ssl=true{$nl}ssl-verify-server-cert=0{$nl}tls-version=TLSv1.2,TLSv1.3",
$header . '--skip-ssl=true --ssl-verify-server-cert=0 --tls-version=TLSv1.2,TLSv1.3',
],
'underscores' => [
"{$nl}ssl-ca=/etc/ssl/ca.pem",
$header . '--ssl_ca=/etc/ssl/ca.pem',
],
'spaces in values' => [
"{$nl}ssl-ca=/path/with spaces/ca.pem{$nl}ssl-key=/other path/key.pem",
$header . '--ssl-ca=/path/with spaces/ca.pem --quick --ssl-key=/other path/key.pem',
],
'windows paths' => [
"{$nl}ssl-ca=C:\\\\Program Files\\\\MySQL\\\\ca.pem",
$header . '--ssl-ca=C:\\Program Files\\MySQL\\ca.pem',
],
'quoted values' => [
"{$nl}ssl-cipher=AES256-SHA",
$header . '--ssl-cipher="AES256-SHA"',
],
'redacted values' => ['', $header . '--ssl-key=*****'],
'not ssl' => ['', $header . '--sslx=1 --skip-sslx --tlsfoo=bar'],
];
}
}
Loading