Commit 98704eb9 authored by xjm's avatar xjm

Issue #2917600 by tedbow, alexpott, catch, anthonyf, xjm, tim.plunkett, Alan...

Issue #2917600 by tedbow, alexpott, catch, anthonyf, xjm, tim.plunkett, Alan D., Berdir, andypost, moshe weitzman: update_fix_compatibility() puts sites into unrecoverable state
parent 0a8ab163
......@@ -80,9 +80,13 @@
* Loads .install files for installed modules to initialize the update system.
*/
function drupal_load_updates() {
/** @var \Drupal\Core\Extension\ModuleExtensionList $extension_list_module */
$extension_list_module = \Drupal::service('extension.list.module');
foreach (drupal_get_installed_schema_version(NULL, FALSE, TRUE) as $module => $schema_version) {
if ($schema_version > -1) {
module_load_install($module);
if ($extension_list_module->exists($module) && !$extension_list_module->checkIncompatibility($module)) {
if ($schema_version > -1) {
module_load_install($module);
}
}
}
}
......
......@@ -14,8 +14,13 @@
/**
* Disables any extensions that are incompatible with the current core version.
*
* @deprecated in Drupal 8.8.4 and is removed from Drupal 9.0.0.
*
* @see https://www.drupal.org/node/3026100
*/
function update_fix_compatibility() {
@trigger_error(__FUNCTION__ . '() is deprecated in Drupal 8.8.4 and will be removed before Drupal 9.0.0. There is no replacement. See https://www.drupal.org/node/3026100', E_USER_DEPRECATED);
// Fix extension objects if the update is being done via Drush 8. In non-Drush
// environments this will already be fixed by the UpdateKernel this point.
UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
......@@ -306,9 +311,11 @@ function update_get_update_list() {
$ret = ['system' => []];
$modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE);
/** @var \Drupal\Core\Extension\ExtensionList $extension_list */
$extension_list = \Drupal::service('extension.list.module');
foreach ($modules as $module => $schema_version) {
// Skip uninstalled and incompatible modules.
if ($schema_version == SCHEMA_UNINSTALLED || update_check_incompatibility($module)) {
if ($schema_version == SCHEMA_UNINSTALLED || $extension_list->checkIncompatibility($module)) {
continue;
}
// Display a requirements error if the user somehow has a schema version
......
......@@ -563,4 +563,21 @@ protected function createExtensionInfo(Extension $extension) {
return $info;
}
/**
* Tests the compatibility of an extension.
*
* @param string $name
* The extension name to check.
*
* @return bool
* TRUE if the extension is incompatible and FALSE if not.
*
* @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
* If there is no extension with the supplied name.
*/
public function checkIncompatibility($name) {
$extension = $this->get($name);
return $extension->info['core_incompatible'] || (isset($extension->info['php']) && version_compare(phpversion(), $extension->info['php']) < 0);
}
}
......@@ -144,7 +144,6 @@ public function handle($op, Request $request) {
require_once $this->root . '/core/includes/update.inc';
drupal_load_updates();
update_fix_compatibility();
if ($request->query->get('continue')) {
$_SESSION['update_ignore_warnings'] = TRUE;
......
......@@ -13,6 +13,7 @@
use Drupal\Core\Cache\Cache;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Path\AliasStorage;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\ContentEntityTypeInterface;
......@@ -31,6 +32,9 @@
*/
function system_requirements($phase) {
global $install_state;
// Reset the extension lists.
\Drupal::service('extension.list.module')->reset();
\Drupal::service('extension.list.theme')->reset();
$requirements = [];
// Report Drupal version
......@@ -795,28 +799,71 @@ function system_requirements($phase) {
}
// Display an error if a newly introduced dependency in a module is not resolved.
if ($phase == 'update') {
if ($phase === 'update'|| $phase === 'runtime') {
$create_extension_incompatibility_list = function ($extension_names, $description, $title) {
// Use an inline twig template to:
// - Concatenate two MarkupInterface objects and preserve safeness.
// - Use the item_list theme for the extension list.
$template = [
'#type' => 'inline_template',
'#template' => '{{ description }}{{ extensions }}',
'#context' => [
'extensions' => [
'#theme' => 'item_list',
],
],
];
$template['#context']['extensions']['#items'] = $extension_names;
$template['#context']['description'] = $description;
return [
'title' => $title,
'value' => [
'list' => $template,
'handbook_link' => [
'#markup' => t(
'Review the <a href=":url"> suggestions for resolving this incompatibility</a> to repair your installation, and then re-run update.php.',
[':url' => 'https://www.drupal.org/docs/8/update/troubleshooting-database-updates']
),
],
],
'severity' => REQUIREMENT_ERROR,
];
};
$profile = drupal_get_profile();
$files = system_rebuild_module_data();
foreach ($files as $module => $file) {
$files += \Drupal::service('extension.list.theme')->getList();
$core_incompatible_extensions = [];
$php_incompatible_extensions = [];
foreach ($files as $extension_name => $file) {
// Ignore disabled modules and installation profiles.
if (!$file->status || $module == $profile) {
if (!$file->status || $extension_name == $profile) {
continue;
}
// Check the module's PHP version.
$name = $file->info['name'];
if (!empty($file->info['core_incompatible'])) {
$core_incompatible_extensions[$file->info['type']][] = $name;
}
// Check the module's PHP version.
$php = $file->info['php'];
if (version_compare($php, PHP_VERSION, '>')) {
$requirements['php']['description'] .= t('@name requires at least PHP @version.', ['@name' => $name, '@version' => $php]);
$requirements['php']['severity'] = REQUIREMENT_ERROR;
$php_incompatible_extensions[$file->info['type']][] = $name;
}
// @todo Remove this 'if' block to allow checking requirements of themes
// https://www.drupal.org/project/drupal/issues/474684.
if ($file->info['type'] !== 'module') {
continue;
}
// Check the module's required modules.
/** @var \Drupal\Core\Extension\Dependency $requirement */
foreach ($file->requires as $requirement) {
$required_module = $requirement->getName();
// Check if the module exists.
if (!isset($files[$required_module])) {
$requirements["$module-$required_module"] = [
$requirements["$extension_name-$required_module"] = [
'title' => t('Unresolved dependency'),
'description' => t('@name requires this module.', ['@name' => $name]),
'value' => t('@required_name (Missing)', ['@required_name' => $required_module]),
......@@ -829,7 +876,7 @@ function system_requirements($phase) {
$required_name = $required_file->info['name'];
$version = str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $required_file->info['version']);
if (!$requirement->isCompatible($version)) {
$requirements["$module-$required_module"] = [
$requirements["$extension_name-$required_module"] = [
'title' => t('Unresolved dependency'),
'description' => t('@name requires this module and version. Currently using @required_name version @version', ['@name' => $name, '@required_name' => $required_name, '@version' => $version]),
'value' => t('@required_name (Version @compatibility required)', ['@required_name' => $required_name, '@compatibility' => $requirement->getConstraintString()]),
......@@ -839,6 +886,115 @@ function system_requirements($phase) {
}
}
}
if (!empty($core_incompatible_extensions['module'])) {
$requirements['module_core_incompatible'] = $create_extension_incompatibility_list(
$core_incompatible_extensions['module'],
new PluralTranslatableMarkup(
count($core_incompatible_extensions['module']),
'The following module is installed, but it is incompatible with Drupal @version:',
'The following modules are installed, but they are incompatible with Drupal @version:',
['@version' => \Drupal::VERSION]
),
new PluralTranslatableMarkup(
count($core_incompatible_extensions['module']),
'Incompatible module',
'Incompatible modules'
)
);
}
if (!empty($core_incompatible_extensions['theme'])) {
$requirements['theme_core_incompatible'] = $create_extension_incompatibility_list(
$core_incompatible_extensions['theme'],
new PluralTranslatableMarkup(
count($core_incompatible_extensions['theme']),
'The following theme is installed, but it is incompatible with Drupal @version:',
'The following themes are installed, but they are incompatible with Drupal @version:',
['@version' => \Drupal::VERSION]
),
new PluralTranslatableMarkup(
count($core_incompatible_extensions['theme']),
'Incompatible theme',
'Incompatible themes'
)
);
}
if (!empty($php_incompatible_extensions['module'])) {
$requirements['module_php_incompatible'] = $create_extension_incompatibility_list(
$php_incompatible_extensions['module'],
new PluralTranslatableMarkup(
count($php_incompatible_extensions['module']),
'The following module is installed, but it is incompatible with PHP @version:',
'The following modules are installed, but they are incompatible with PHP @version:',
['@version' => phpversion()]
),
new PluralTranslatableMarkup(
count($php_incompatible_extensions['module']),
'Incompatible module',
'Incompatible modules'
)
);
}
if (!empty($php_incompatible_extensions['theme'])) {
$requirements['theme_php_incompatible'] = $create_extension_incompatibility_list(
$php_incompatible_extensions['theme'],
new PluralTranslatableMarkup(
count($php_incompatible_extensions['theme']),
'The following theme is installed, but it is incompatible with PHP @version:',
'The following themes are installed, but they are incompatible with PHP @version:',
['@version' => phpversion()]
),
new PluralTranslatableMarkup(
count($php_incompatible_extensions['theme']),
'Incompatible theme',
'Incompatible themes'
)
);
}
// Look for invalid modules.
$extension_config = \Drupal::configFactory()->get('core.extension');
/** @var \Drupal\Core\Extension\ExtensionList $extension_list */
$extension_list = \Drupal::service('extension.list.module');
$is_missing_extension = function ($extension_name) use (&$extension_list) {
return !$extension_list->exists($extension_name);
};
$invalid_modules = array_filter(array_keys($extension_config->get('module')), $is_missing_extension);
if (!empty($invalid_modules)) {
$requirements['invalid_module'] = $create_extension_incompatibility_list(
$invalid_modules,
new PluralTranslatableMarkup(
count($invalid_modules),
'The following module is marked as installed in the core.extension configuration, but it is missing:',
'The following modules are marked as installed in the core.extension configuration, but they are missing:'
),
new PluralTranslatableMarkup(
count($invalid_modules),
'Missing or invalid module',
'Missing or invalid modules'
)
);
}
// Look for invalid themes.
$extension_list = \Drupal::service('extension.list.theme');
$invalid_themes = array_filter(array_keys($extension_config->get('theme')), $is_missing_extension);
if (!empty($invalid_themes)) {
$requirements['invalid_theme'] = $create_extension_incompatibility_list(
$invalid_themes,
new PluralTranslatableMarkup(
count($invalid_themes),
'The following theme is marked as installed in the core.extension configuration, but it is missing:',
'The following themes are marked as installed in the core.extension configuration, but they are missing:'
),
new PluralTranslatableMarkup(
count($invalid_themes),
'Missing or invalid theme',
'Missing or invalid themes'
)
);
}
}
// Returns Unicode library status and errors.
......
......@@ -8,6 +8,7 @@
* Tests that extensions that are incompatible with the current core version are disabled.
*
* @group Update
* @group legacy
*/
class CompatibilityFixTest extends KernelTestBase {
......@@ -21,6 +22,9 @@ protected function setUp() {
require_once $this->root . '/core/includes/update.inc';
}
/**
* @expectedDeprecation update_fix_compatibility() is deprecated in Drupal 8.8.4 and will be removed before Drupal 9.0.0. There is no replacement. See https://www.drupal.org/node/3026100
*/
public function testFixCompatibility() {
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
......
......@@ -206,9 +206,77 @@ public function testReset() {
}
/**
* @covers ::checkIncompatibility
*
* @dataProvider providerCheckIncompatibility
*/
public function testCheckIncompatibility($additional_settings, $expected) {
$test_extension_list = $this->setupTestExtensionList(['test_name'], $additional_settings);
$this->assertSame($expected, $test_extension_list->checkIncompatibility('test_name'));
}
/**
* DataProvider for testCheckIncompatibility().
*/
public function providerCheckIncompatibility() {
return [
'core_incompatible true' => [
[
'core_incompatible' => TRUE,
],
TRUE,
],
'core_incompatible false' => [
[
'core_incompatible' => FALSE,
],
FALSE,
],
'PHP 1, core_incompatible FALSE' => [
[
'core_incompatible' => FALSE,
'php' => 1,
],
FALSE,
],
'PHP 1000000000000, core_incompatible FALSE' => [
[
'core_incompatible' => FALSE,
'php' => 1000000000000,
],
TRUE,
],
'PHP 1, core_incompatible TRUE' => [
[
'core_incompatible' => TRUE,
'php' => 1,
],
TRUE,
],
'PHP 1000000000000, core_incompatible TRUE' => [
[
'core_incompatible' => TRUE,
'php' => 1000000000000,
],
TRUE,
],
];
}
/**
* Sets up an a test extension list.
*
* @param string[] $extension_names
* The names of the extensions to create.
* @param mixed[] $additional_info_values
* The additional values to add to extensions info.yml files. These values
* will be encoded using '\Drupal\Component\Serialization\Yaml::encode()'.
* The array keys should be valid top level yaml file keys.
*
* @return \Drupal\Tests\Core\Extension\TestExtension
* The test extension list.
*/
protected function setupTestExtensionList($extension_names = ['test_name']) {
protected function setupTestExtensionList(array $extension_names = ['test_name'], array $additional_info_values = []) {
vfsStream::setup('drupal_root');
$folders = ['example' => []];
......@@ -217,7 +285,7 @@ protected function setupTestExtensionList($extension_names = ['test_name']) {
'name' => 'test name',
'type' => 'test_extension',
'core' => '8.x',
]);
] + $additional_info_values);
}
vfsStream::create($folders);
foreach ($extension_names as $extension_name) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment