diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 462699b..104fecf 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -741,6 +741,20 @@ public function schema() { } /** + * Escapes a database name string. + * + * Force all database names to be strictly alphanumeric-plus-underscore. + * For some database drivers, it may also wrap the database name in + * database-specific escape characters. + * + * @return string + * The sanitized database name string. + */ + public function escapeDatabase($database) { + return preg_replace('/[^A-Za-z0-9_.]+/', '', $database); + } + + /** * Escapes a table name string. * * Force all table names to be strictly alphanumeric-plus-underscore. @@ -1088,6 +1102,16 @@ public function supportsTransactionalDDL() { */ abstract public function databaseType(); + /** + * Creates a database. + * + * In order to use this method, you must be connected without a database + * specified. + * + * @param string $database + * The name of the database to create. + */ + abstract public function createDatabase($database); /** * Gets any special processing requirements for the condition operator. diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php index ae24176..128780f 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php @@ -10,6 +10,7 @@ use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Database\Database; +use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\TransactionCommitFailedException; use Drupal\Core\Database\DatabaseException; use Drupal\Core\Database\Connection as DatabaseConnection; @@ -24,6 +25,11 @@ class Connection extends DatabaseConnection { /** + * Error code for "Unknown database" error. + */ + const DATABASE_NOT_FOUND = 1049; + + /** * Flag to indicate if the cleanup function in __destruct() should run. * * @var boolean @@ -47,7 +53,9 @@ public function __construct(array $connection_options = array()) { // Default to TCP connection on port 3306. $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']); } - $dsn .= ';dbname=' . $connection_options['database']; + if (!empty($connection_options['database'])) { + $dsn .= ';dbname=' . $connection_options['database']; + } // Allow PDO options to be overridden. $connection_options += array( 'pdo' => array(), @@ -113,6 +121,28 @@ public function databaseType() { return 'mysql'; } + /** + * Overrides \Drupal\Core\Database\Connection::createDatabase(). + * + * @param string $database + * The name of the database to create. + * + * @throws DatabaseNotFoundException + */ + public function createDatabase($database) { + // Escape the database name. + $database = Database::getConnection()->escapeDatabase($database); + + try { + // Create the database and set it as active. + $this->exec("CREATE DATABASE $database"); + $this->exec("USE $database"); + } + catch (\Exception $e) { + throw new DatabaseNotFoundException($e->getMessage()); + } + } + public function mapConditionOperator($operator) { // We don't want to override any of the defaults. return NULL; diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php index 4835103..0f09aae 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php @@ -8,6 +8,9 @@ namespace Drupal\Core\Database\Driver\mysql\Install; use Drupal\Core\Database\Install\Tasks as InstallTasks; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Driver\mysql\Connection; +use Drupal\Core\Database\DatabaseNotFoundException; /** * Specifies installation tasks for MySQL and equivalent databases. @@ -33,4 +36,50 @@ public function name() { public function minimumVersion() { return '5.0.15'; } + + /** + * Check database connection and attempt to create database if the database is + * missing. + */ + protected function connect() { + try { + // This doesn't actually test the connection. + db_set_active(); + // Now actually do a check. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (\Exception $e) { + // Attempt to create the database if it is not found. + if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { + // Remove the database string from connection info. + $connection_info = Database::getConnectionInfo(); + $database = $connection_info['default']['database']; + unset($connection_info['default']['database']); + + // In order to change the Database::$databaseInfo array, need to remove + // the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + try { + // Now, attempt the connection again; if it's successful, attempt to + // create the database. + Database::getConnection()->createDatabase($database); + } + catch (DatabaseNotFoundException $e) { + // Still no dice; probably a permission issue. Raise the error to the + // installer. + $this->fail(st('Database %database not found. The server reports the following message when attempting to create the database: %error.', array('%database' => $database, '%error' => $e->getMessage()))); + } + } + else { + // Database connection failed for some other reason than the database + // not existing. + $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.', array('%error' => $e->getMessage()))); + return FALSE; + } + } + return TRUE; + } } diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php index 91d95ff..c278004 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php @@ -9,8 +9,10 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\Connection as DatabaseConnection; +use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\StatementInterface; +use Locale; use PDO; use PDOException; @@ -26,6 +28,11 @@ class Connection extends DatabaseConnection { */ const POSTGRESQL_NEXTID_LOCK = 1000; + /** + * Error code for "Unknown database" error. + */ + const DATABASE_NOT_FOUND = 7; + public function __construct(array $connection_options = array()) { // This driver defaults to transaction support, except if explicitly passed FALSE. $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE); @@ -55,6 +62,7 @@ public function __construct(array $connection_options = array()) { $this->connectionOptions = $connection_options; + $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1'); $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port']; // Allow PDO options to be overridden. @@ -167,6 +175,36 @@ public function databaseType() { return 'pgsql'; } + /** + * Overrides \Drupal\Core\Database\Connection::createDatabase(). + * + * @param string $database + * The name of the database to create. + * + * @throws DatabaseNotFoundException + */ + public function createDatabase($database) { + // Escape the database name. + $database = Database::getConnection()->escapeDatabase($database); + + // If the PECL intl extension is installed, use it to determine the proper + // locale. Otherwise, fall back to en_US. + if (class_exists('Locale')) { + $locale = Locale::getDefault(); + } + else { + $locale = 'en_US'; + } + + try { + // Create the database and set it as active. + $this->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'"); + } + catch (\Exception $e) { + throw new DatabaseNotFoundException($e->getMessage()); + } + } + public function mapConditionOperator($operator) { static $specials = array( // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php index 6dc40a2..78b7d06 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php @@ -9,6 +9,8 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\Install\Tasks as InstallTasks; +use Drupal\Core\Database\Driver\pgsql\Connection; +use Drupal\Core\Database\DatabaseNotFoundException; use Exception; @@ -42,6 +44,62 @@ public function minimumVersion() { } /** + * Check database connection and attempt to create database if the database is + * missing. + */ + protected function connect() { + try { + // This doesn't actually test the connection. + db_set_active(); + // Now actually do a check. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (Exception $e) { + // Attempt to create the database if it is not found. + if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { + // Remove the database string from connection info. + $connection_info = Database::getConnectionInfo(); + $database = $connection_info['default']['database']; + unset($connection_info['default']['database']); + + // In order to change the Database::$databaseInfo array, need to remove + // the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + try { + // Now, attempt the connection again; if it's successful, attempt to + // create the database. + Database::getConnection()->createDatabase($database); + Database::closeConnection(); + + // Now, restore the database config. + Database::removeConnection('default'); + $connection_info['default']['database'] = $database; + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Check the database connection. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (DatabaseNotFoundException $e) { + // Still no dice; probably a permission issue. Raise the error to the + // installer. + $this->fail(st('Database %database not found. The server reports the following message when attempting to create the database: %error.', array('%database' => $database, '%error' => $e->getMessage()))); + } + } + else { + // Database connection failed for some other reason than the database + // not existing. + $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.', array('%error' => $e->getMessage()))); + return FALSE; + } + } + return TRUE; + } + + /** * Check encoding is UTF8. */ protected function checkEncoding() { diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php index 02a55a4..abda556 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Database\Driver\sqlite; use Drupal\Core\Database\Database; +use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\TransactionNoActiveException; use Drupal\Core\Database\TransactionNameNonUniqueException; use Drupal\Core\Database\TransactionCommitFailedException; @@ -16,6 +17,7 @@ use PDO; use Exception; +use SplFileInfo; /** * Specific SQLite implementation of DatabaseConnection. @@ -33,6 +35,11 @@ class Connection extends DatabaseConnection { protected $savepointSupport = FALSE; /** + * Error code for "Unable to open database file" error. + */ + const DATABASE_NOT_FOUND = 14; + + /** * Whether or not the active transaction (if any) will be rolled back. * * @var boolean @@ -267,6 +274,22 @@ public function databaseType() { return 'sqlite'; } + /** + * Overrides \Drupal\Core\Database\Connection::createDatabase(). + * + * @param string $database + * The name of the database to create. + * + * @throws DatabaseNotFoundException + */ + public function createDatabase($database) { + // Verify the database is writable. + $db_directory = new SplFileInfo(dirname($database)); + if (!$db_directory->isDir() && !drupal_mkdir($db_directory->getPathName(), 0755, TRUE)) { + throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName()); + } + } + public function mapConditionOperator($operator) { // We don't want to override any of the defaults. static $specials = array( diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php index 4f0e16b..5bf2642 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php @@ -7,9 +7,12 @@ namespace Drupal\Core\Database\Driver\sqlite\Install; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\Install\Tasks as InstallTasks; -use SplFileInfo; +use Exception; class Tasks extends InstallTasks { protected $pdoDriver = 'sqlite'; @@ -41,17 +44,61 @@ public function getFormOptions($database) { return $form; } - public function validateDatabaseSettings($database) { - // Perform standard validation. - $errors = parent::validateDatabaseSettings($database); - - // Verify the database is writable. - $db_directory = new SplFileInfo(dirname($database['database'])); - if (!$db_directory->isWritable()) { - $errors[$database['driver'] . '][database'] = st('The directory you specified is not writable by the web server.'); + /** + * Check database connection and attempt to create database if the database is + * missing. + */ + protected function connect() { + try { + // This doesn't actually test the connection. + db_set_active(); + // Now actually do a check. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); } + catch (Exception $e) { + // Attempt to create the database if it is not found. + if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { + // Remove the database string from connection info. + $connection_info = Database::getConnectionInfo(); + $database = $connection_info['default']['database']; + + // We cannot use file_directory_temp() here because we haven't yet + // successfully connected to the database. + $connection_info['default']['database'] = drupal_tempnam(sys_get_temp_dir(), 'sqlite'); - return $errors; + // In order to change the Database::$databaseInfo array, need to remove + // the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + try { + Database::getConnection()->createDatabase($database); + Database::closeConnection(); + + // Now, restore the database config. + Database::removeConnection('default'); + $connection_info['default']['database'] = $database; + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Check the database connection. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (DatabaseNotFoundException $e) { + // Still no dice; probably a permission issue. Raise the error to the + // installer. + $this->fail(st('Database %database not found. The server reports the following message when attempting to create the database: %error.', array('%database' => $database, '%error' => $e->getMessage()))); + } + } + else { + // Database connection failed for some other reason than the database + // not existing. + $this->fail(st('Failed to connect to your database server. The server reports the following message: %error.', array('%error' => $e->getMessage()))); + return FALSE; + } + } + return TRUE; } }