Skip to content
Snippets Groups Projects
Commit f838dbc8 authored by catch's avatar catch
Browse files

Issue #3129534 by daffie, anmolgoyal74, mondrake, naresh_bavaskar, Beakerboy,...

Issue #3129534 by daffie, anmolgoyal74, mondrake, naresh_bavaskar, Beakerboy, catch, alexpott, quietone: Automatically enable the module that is providing the current database driver
parent d722e9d7
No related branches found
No related tags found
6 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!1012Issue #3226887: Hreflang on non-canonical content pages,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10,!596Issue #3046532: deleting an entity reference field, used in a contextual view, makes the whole site unrecoverable,!496Issue #2463967: Use .user.ini file for PHP settings,!16Draft: Resolve #2081585 "History storage"
......@@ -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']
......
......@@ -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);
......
......@@ -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';
}
}
<?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;
}
}
<?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);
}
}
}
......@@ -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')) {
......
<?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.');
}
}
......@@ -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']);
}
}
......@@ -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));
......
<?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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment