From f838dbc871a800c0a99bf83f11de850a42422cab Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 18 Jan 2021 21:43:55 +0000 Subject: [PATCH] Issue #3129534 by daffie, anmolgoyal74, mondrake, naresh_bavaskar, Beakerboy, catch, alexpott, quietone: Automatically enable the module that is providing the current database driver --- core/core.services.yml | 6 ++ core/includes/install.inc | 13 +++ core/lib/Drupal/Core/Database/Connection.php | 18 ++++ .../DatabaseDriverUninstallValidator.php | 68 ++++++++++++++ .../DatabaseDriverUninstallValidator.php | 88 +++++++++++++++++++ core/modules/system/system.install | 18 ++++ .../DatabaseDriverProvidedByModuleTest.php | 72 +++++++++++++++ .../InstallerNonDefaultDatabaseDriverTest.php | 28 ++++++ .../Drupal/KernelTests/KernelTestBase.php | 17 +++- ...KernelTestBaseDatabaseDriverModuleTest.php | 68 ++++++++++++++ 10 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 core/lib/Drupal/Core/Extension/DatabaseDriverUninstallValidator.php create mode 100644 core/lib/Drupal/Core/ProxyClass/Extension/DatabaseDriverUninstallValidator.php create mode 100644 core/modules/system/tests/src/Functional/System/DatabaseDriverProvidedByModuleTest.php create mode 100644 core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 4f1b5ab35c4f..9dc264e5bee9 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -556,6 +556,12 @@ services: - { name: module_install.uninstall_validator } arguments: ['@string_translation', '@extension.list.module', '@extension.list.theme'] lazy: true + database_driver_uninstall_validator: + class: Drupal\Core\Extension\DatabaseDriverUninstallValidator + tags: + - { name: module_install.uninstall_validator } + arguments: ['@string_translation', '@extension.list.module', '@database'] + lazy: true theme_handler: class: Drupal\Core\Extension\ThemeHandler arguments: ['%app.root%', '@config.factory', '@extension.list.theme'] diff --git a/core/includes/install.inc b/core/includes/install.inc index 8e6eb21d81ad..3983773e81d4 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -8,6 +8,7 @@ use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Database\Database; use Drupal\Core\Extension\Dependency; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Installer\InstallerKernel; @@ -619,6 +620,18 @@ function drupal_install_system($install_state) { ->set('profile', $install_state['parameters']['profile']) ->save(); + $connection = Database::getConnection(); + $provider = $connection->getProvider(); + // When the database driver is provided by a module, then install that module. + // This module must be installed before any other module, as it must be able + // to override any call to hook_schema() or any "backend_overridable" service. + if ($provider !== 'core') { + $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; + if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) { + $kernel->getContainer()->get('module_installer')->install([$provider], FALSE); + } + } + // Install System module. $kernel->getContainer()->get('module_installer')->install(['system'], FALSE); diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 486d788b2c05..3f194e1aa052 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -1950,4 +1950,22 @@ public static function createUrlFromConnectionOptions(array $connection_options) return $db_url; } + /** + * Get the module name of the module that is providing the database driver. + * + * @return string + * The module name of the module that is providing the database driver, or + * "core" when the driver is not provided as part of a module. + */ + public function getProvider(): string { + [$first, $second] = explode('\\', $this->connectionOptions['namespace'], 3); + + // The namespace for Drupal modules is Drupal\MODULE_NAME, and the module + // name must be all lowercase. Second-level namespaces containing uppercase + // letters (e.g., "Core", "Component", "Driver") are not modules. + // @see \Drupal\Core\DrupalKernel::getModuleNamespacesPsr4() + // @see https://www.drupal.org/docs/8/creating-custom-modules/naming-and-placing-your-drupal-8-module#s-name-your-module + return ($first === 'Drupal' && strtolower($second) === $second) ? $second : 'core'; + } + } diff --git a/core/lib/Drupal/Core/Extension/DatabaseDriverUninstallValidator.php b/core/lib/Drupal/Core/Extension/DatabaseDriverUninstallValidator.php new file mode 100644 index 000000000000..ed741de3c4dd --- /dev/null +++ b/core/lib/Drupal/Core/Extension/DatabaseDriverUninstallValidator.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\Core\Extension; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Database; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; + +/** + * Ensures installed modules providing a database driver are not uninstalled. + */ +class DatabaseDriverUninstallValidator implements ModuleUninstallValidatorInterface { + + use StringTranslationTrait; + + /** + * The module extension list. + * + * @var \Drupal\Core\Extension\ModuleExtensionList + */ + protected $moduleExtensionList; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * Constructs a new DatabaseDriverUninstallValidator. + * + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * The string translation service. + * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module + * The module extension list. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + */ + public function __construct(TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, Connection $connection) { + $this->stringTranslation = $string_translation; + $this->moduleExtensionList = $extension_list_module; + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function validate($module) { + $reasons = []; + + // @todo Remove the next line of code in + // https://www.drupal.org/project/drupal/issues/3129043. + $this->connection = Database::getConnection(); + + // When the database driver is provided by a module, then that module + // cannot be uninstalled. + if ($module === $this->connection->getProvider()) { + $module_name = $this->moduleExtensionList->get($module)->info['name']; + $reasons[] = $this->t("The module '@module_name' is providing the database driver '@driver_name'.", + ['@module_name' => $module_name, '@driver_name' => $this->connection->driver()]); + } + + return $reasons; + } + +} diff --git a/core/lib/Drupal/Core/ProxyClass/Extension/DatabaseDriverUninstallValidator.php b/core/lib/Drupal/Core/ProxyClass/Extension/DatabaseDriverUninstallValidator.php new file mode 100644 index 000000000000..2390c26f8c5e --- /dev/null +++ b/core/lib/Drupal/Core/ProxyClass/Extension/DatabaseDriverUninstallValidator.php @@ -0,0 +1,88 @@ +<?php +// @codingStandardsIgnoreFile + +/** + * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Extension\DatabaseDriverUninstallValidator' "core/lib/Drupal/Core". + */ + +namespace Drupal\Core\ProxyClass\Extension { + + /** + * Provides a proxy class for \Drupal\Core\Extension\DatabaseDriverUninstallValidator. + * + * @see \Drupal\Component\ProxyBuilder + */ + class DatabaseDriverUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface + { + + use \Drupal\Core\DependencyInjection\DependencySerializationTrait; + + /** + * The id of the original proxied service. + * + * @var string + */ + protected $drupalProxyOriginalServiceId; + + /** + * The real proxied service, after it was lazy loaded. + * + * @var \Drupal\Core\Extension\DatabaseDriverUninstallValidator + */ + protected $service; + + /** + * The service container. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $container; + + /** + * Constructs a ProxyClass Drupal proxy object. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The container. + * @param string $drupal_proxy_original_service_id + * The service ID of the original service. + */ + public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id) + { + $this->container = $container; + $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; + } + + /** + * Lazy loads the real service from the container. + * + * @return object + * Returns the constructed real service. + */ + protected function lazyLoadItself() + { + if (!isset($this->service)) { + $this->service = $this->container->get($this->drupalProxyOriginalServiceId); + } + + return $this->service; + } + + /** + * {@inheritdoc} + */ + public function validate($module) + { + return $this->lazyLoadItself()->validate($module); + } + + /** + * {@inheritdoc} + */ + public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation) + { + return $this->lazyLoadItself()->setStringTranslation($translation); + } + + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index e4cb7cef7c09..2b1a2a59b395 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1126,6 +1126,24 @@ function system_requirements($phase) { } } + // When the database driver is provided by a module, then check that the + // providing module is enabled. + if ($phase === 'runtime' || $phase === 'update') { + $connection = Database::getConnection(); + $provider = $connection->getProvider(); + if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) { + $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; + if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) { + $requirements['database_driver_provided_by_module'] = [ + 'title' => t('Database driver provided by module'), + 'value' => t('Not enabled'), + 'description' => t('The current database driver is provided by the module: %module. The module is currently not enabled. You should immediately <a href=":enable">enable</a> the module.', ['%module' => $provider, ':enable' => Url::fromRoute('system.modules_list')->toString()]), + 'severity' => REQUIREMENT_ERROR, + ]; + } + } + } + // Check xdebug.max_nesting_level, as some pages will not work if it is too // low. if (extension_loaded('xdebug')) { diff --git a/core/modules/system/tests/src/Functional/System/DatabaseDriverProvidedByModuleTest.php b/core/modules/system/tests/src/Functional/System/DatabaseDriverProvidedByModuleTest.php new file mode 100644 index 000000000000..e81ced70d303 --- /dev/null +++ b/core/modules/system/tests/src/Functional/System/DatabaseDriverProvidedByModuleTest.php @@ -0,0 +1,72 @@ +<?php + +namespace Drupal\Tests\system\Functional\System; + +use Drupal\Core\Database\Database; +use Drupal\Tests\BrowserTestBase; + +/** + * Tests output on the status overview page. + * + * @group system + */ +class DatabaseDriverProvidedByModuleTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $admin_user = $this->drupalCreateUser([ + 'administer site configuration', + ]); + $this->drupalLogin($admin_user); + } + + /** + * Tests that the status page shows the error message. + */ + public function testDatabaseDriverIsProvidedByModuleButTheModuleIsNotEnabled(): void { + $driver = Database::getConnection()->driver(); + if (!in_array($driver, ['mysql', 'pgsql'])) { + $this->markTestSkipped("This test does not support the {$driver} database driver."); + } + + // Change the default database connection to use the one from the module + // driver_test. + $connection_info = Database::getConnectionInfo(); + $database = [ + 'database' => $connection_info['default']['database'], + 'username' => $connection_info['default']['username'], + 'password' => $connection_info['default']['password'], + 'prefix' => $connection_info['default']['prefix'], + 'host' => $connection_info['default']['host'], + 'driver' => 'Drivertest' . ucfirst($driver), + 'namespace' => 'Drupal\\driver_test\\Driver\\Database\\Drivertest' . ucfirst($driver), + 'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/Drivertest' . ucfirst($driver), + ]; + if (isset($connection_info['default']['port'])) { + $database['port'] = $connection_info['default']['port']; + } + $settings['databases']['default']['default'] = (object) [ + 'value' => $database, + 'required' => TRUE, + ]; + $this->writeSettings($settings); + + $this->drupalGet('admin/reports/status'); + $this->assertSession()->statusCodeEquals(200); + + // The module driver_test is not enabled and is providing to current + // database driver. Check that the correct error is shown. + $this->assertSession()->pageTextContains('Database driver provided by module'); + $this->assertSession()->pageTextContains('The current database driver is provided by the module: driver_test. The module is currently not enabled. You should immediately enable the module.'); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php index 2aae61144517..f29ecde22c28 100644 --- a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php @@ -3,6 +3,8 @@ namespace Drupal\FunctionalTests\Installer; use Drupal\Core\Database\Database; +use Drupal\Core\Extension\Extension; +use Drupal\Core\Extension\ModuleUninstallValidatorException; /** * Tests the interactive installer. @@ -60,6 +62,32 @@ public function testInstalled() { $this->assertStringContainsString("'namespace' => 'Drupal\\\\driver_test\\\\Driver\\\\Database\\\\{$this->testDriverName}',", $contents); $this->assertStringContainsString("'driver' => '{$this->testDriverName}',", $contents); $this->assertStringContainsString("'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/',", $contents); + + // Assert that the module "driver_test" has been installed. + $this->assertEquals(\Drupal::service('module_handler')->getModule('driver_test'), new Extension($this->root, 'module', 'core/modules/system/tests/modules/driver_test/driver_test.info.yml')); + + // Change the default database connection to use the database driver from + // the module "driver_test". + $connection_info = Database::getConnectionInfo(); + $driver_test_connection = $connection_info['default']; + $driver_test_connection['driver'] = $this->testDriverName; + $driver_test_connection['namespace'] = 'Drupal\\driver_test\\Driver\\Database\\' . $this->testDriverName; + $driver_test_connection['autoload'] = "core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/"; + Database::renameConnection('default', 'original_database_connection'); + Database::addConnectionInfo('default', 'default', $driver_test_connection); + + // The module "driver_test" should not be uninstallable, because it is + // providing the database driver. + try { + $this->container->get('module_installer')->uninstall(['driver_test']); + $this->fail('Uninstalled driver_test module.'); + } + catch (ModuleUninstallValidatorException $e) { + $this->assertStringContainsString("The module 'Contrib database driver test' is providing the database driver '{$this->testDriverName}'.", $e->getMessage()); + } + + // Restore the old database connection. + Database::addConnectionInfo('default', 'default', $connection_info['default']); } } diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index a5edce16dfae..e5afa6bba918 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -336,6 +336,20 @@ private function bootKernel() { $modules = self::getModulesToEnable(static::class); + // When a module is providing the database driver, then enable that module. + $connection_info = Database::getConnectionInfo(); + $driver = $connection_info['default']['driver']; + $namespace = $connection_info['default']['namespace'] ?? NULL; + $autoload = $connection_info['default']['autoload'] ?? NULL; + if (strpos($autoload, 'src/Driver/Database/') !== FALSE) { + [$first, $second] = explode('\\', $namespace, 3); + if ($first === 'Drupal' && strtolower($second) === $second) { + // Add the module that provides the database driver to the list of + // modules as the first to be enabled. + array_unshift($modules, $second); + } + } + // Bootstrap the kernel. Do not use createFromRequest() to retain Settings. $kernel = new DrupalKernel('testing', $this->classLoader, FALSE); $kernel->setSitePath($this->siteDirectory); @@ -357,9 +371,6 @@ private function bootKernel() { // Ensure database tasks have been run. require_once __DIR__ . '/../../../includes/install.inc'; - $connection_info = Database::getConnectionInfo(); - $driver = $connection_info['default']['driver']; - $namespace = $connection_info['default']['namespace'] ?? NULL; $errors = db_installer_object($driver, $namespace)->runTasks(); if (!empty($errors)) { $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors)); diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php new file mode 100644 index 000000000000..b72e2972e26b --- /dev/null +++ b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\KernelTests; + +use Drupal\Core\Database\Database; + +/** + * @coversDefaultClass \Drupal\KernelTests\KernelTestBase + * + * @group PHPUnit + * @group Test + * @group KernelTests + */ +class KernelTestBaseDatabaseDriverModuleTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected function getDatabaseConnectionInfo() { + // If the test is run with argument SIMPLETEST_DB then use it. + $db_url = getenv('SIMPLETEST_DB'); + if (empty($db_url)) { + throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.'); + } + else { + $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root); + + if (in_array($database['driver'], ['mysql', 'pgsql'])) { + // Change the used database driver to the one provided by the module + // "driver_test". + $driver = 'Drivertest' . ucfirst($database['driver']); + $database['driver'] = $driver; + $database['namespace'] = 'Drupal\\driver_test\\Driver\\Database\\' . $driver; + $database['autoload'] = "core/modules/system/tests/modules/driver_test/src/Driver/Database/$driver/"; + } + + Database::addConnectionInfo('default', 'default', $database); + } + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + if (!empty($connection_info)) { + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + // Replace the full table prefix definition to ensure that no table + // prefixes of the test runner leak into the test. + $connection_info[$target]['prefix'] = [ + 'default' => $this->databasePrefix, + ]; + } + } + return $connection_info; + } + + /** + * @covers ::bootEnvironment + */ + public function testDatabaseDriverModuleEnabled(): void { + $driver = Database::getConnection()->driver(); + if (!in_array($driver, ['DrivertestMysql', 'DrivertestPgsql'])) { + $this->markTestSkipped("This test does not support the {$driver} database driver."); + } + + // Test that the module that is providing the database driver is enabled. + $this->assertSame(1, \Drupal::service('extension.list.module')->get('driver_test')->status); + } + +} -- GitLab