diff --git a/core/includes/install.inc b/core/includes/install.inc index 66fffd55ae..2c5d4167cd 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -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); + } } } } diff --git a/core/includes/update.inc b/core/includes/update.inc index 9e3a7a4e34..a42cc1b6fd 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -9,13 +9,19 @@ */ use Drupal\Component\Graph\Graph; +use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Update\UpdateKernel; use Drupal\Core\Utility\Error; /** * Disables any extensions that are incompatible with the current core version. + * + * @deprecated in Drupal 8.8.2 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.2 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()); @@ -24,7 +30,13 @@ function update_fix_compatibility() { $save = FALSE; foreach (['module', 'theme'] as $type) { foreach ($extension_config->get($type) as $name => $weight) { - if (update_check_incompatibility($name, $type)) { + try { + $is_incompatible_or_missing = update_check_incompatibility($name, $type); + } + catch (UnknownExtensionException $exception) { + $is_incompatible_or_missing = TRUE; + } + if ($is_incompatible_or_missing) { $extension_config->clear("$type.$name"); $save = TRUE; } @@ -40,27 +52,11 @@ function update_fix_compatibility() { * Tests the compatibility of a module or theme. */ function update_check_incompatibility($name, $type = 'module') { - static $themes, $modules; - - // Store values of expensive functions for future use. - if (empty($themes) || empty($modules)) { - // We need to do a full rebuild here to make sure the database reflects any - // code changes that were made in the filesystem before the update script - // was initiated. - $themes = \Drupal::service('theme_handler')->rebuildThemeData(); - $modules = \Drupal::service('extension.list.module')->reset()->getList(); - } - - if ($type == 'module' && isset($modules[$name])) { - $file = $modules[$name]; - } - elseif ($type == 'theme' && isset($themes[$name])) { - $file = $themes[$name]; + if ($type === 'theme') { + return \Drupal::service('extension.list.theme')->checkIncompatibility($name); } - if (!isset($file) - || $file->info['core_incompatible'] - || version_compare(phpversion(), $file->info['php']) < 0) { - return TRUE; + if ($type === 'module') { + return \Drupal::service('extension.list.module')->checkIncompatibility($name); } return FALSE; } @@ -306,9 +302,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 diff --git a/core/lib/Drupal/Core/Extension/ExtensionList.php b/core/lib/Drupal/Core/Extension/ExtensionList.php index 8fc8da0ca0..a6c28a49a0 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionList.php +++ b/core/lib/Drupal/Core/Extension/ExtensionList.php @@ -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); + } + } diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 25a2a70891..8c476329c2 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -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; diff --git a/core/modules/system/system.install b/core/modules/system/system.install index b1c6772f07..e6809f21b6 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -21,6 +21,7 @@ use Drupal\Core\Extension\Extension; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\path_alias\Entity\PathAlias; use Drupal\path_alias\PathAliasStorage; use Drupal\Core\Site\Settings; @@ -35,6 +36,35 @@ */ function system_requirements($phase) { global $install_state; + $create_extension_incompatibility_list = function ($extension_names, $singular_description, $plural_description, $singular_title, $plural_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'] = new PluralTranslatableMarkup(count($extension_names), $singular_description, $plural_description); + return [ + 'title' => new PluralTranslatableMarkup(count($extension_names), $singular_title, $plural_title), + 'value' => [ + 'list' => $template, + 'handbook_link' => [ + '#markup' => t( + 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.', + [':url' => 'https://www.drupal.org/docs/8/update/troubleshooting-database-updates'] + ), + ], + ], + 'severity' => REQUIREMENT_ERROR, + ]; + }; $requirements = []; // Report Drupal version @@ -858,28 +888,42 @@ 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') { $profile = \Drupal::installProfile(); $files = \Drupal::service('extension.list.module')->getList(); - foreach ($files as $module => $file) { - // Ignore disabled modules and installation profiles. - if (!$file->status || $module == $profile) { + $files += \Drupal::service('extension.list.theme')->getList(); + $core_incompatible_extensions = []; + $php_incompatible_extensions = []; + foreach ($files as $extension_name => $file) { + // Ignore uninstalled extensions and installation profiles. + 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]), @@ -892,8 +936,8 @@ 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"] = [ - 'title' => t('Unresolved dependency'), + $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()]), 'severity' => REQUIREMENT_ERROR, @@ -902,6 +946,42 @@ function system_requirements($phase) { } } } + if (!empty($core_incompatible_extensions['module'])) { + $requirements['module_core_incompatible'] = $create_extension_incompatibility_list( + $core_incompatible_extensions['module'], + 'The following module is installed, but it is incompatible with Drupal ' . \Drupal::VERSION . ':', + 'The following modules are installed, but they are incompatible with Drupal ' . \Drupal::VERSION . ':', + 'Incompatible module', + 'Incompatible modules' + ); + } + if (!empty($core_incompatible_extensions['theme'])) { + $requirements['theme_core_incompatible'] = $create_extension_incompatibility_list( + $core_incompatible_extensions['theme'], + 'The following theme is installed, but it is incompatible with Drupal ' . \Drupal::VERSION . ':', + 'The following themes are installed, but they are incompatible with Drupal ' . \Drupal::VERSION . ':', + 'Incompatible theme', + 'Incompatible themes' + ); + } + if (!empty($php_incompatible_extensions['module'])) { + $requirements['module_php_incompatible'] = $create_extension_incompatibility_list( + $php_incompatible_extensions['module'], + 'The following module is installed, but it is incompatible with PHP ' . phpversion() . ':', + 'The following modules are installed, but they are incompatible with PHP ' . phpversion() . ':', + 'Incompatible module', + 'Incompatible modules' + ); + } + if (!empty($php_incompatible_extensions['theme'])) { + $requirements['theme_php_incompatible\''] = $create_extension_incompatibility_list( + $php_incompatible_extensions['theme'], + 'The following theme is installed, but it is incompatible with PHP ' . phpversion() . ':', + 'The following themes are installed, but they are incompatible with PHP ' . phpversion() . ':', + 'PHP incompatible theme', + 'PHP incompatible themes' + ); + } } // Returns Unicode library status and errors. @@ -1112,6 +1192,42 @@ function system_requirements($phase) { } } + if ($phase === 'runtime' || $phase === 'update') { + $extension_config = \Drupal::configFactory()->get('core.extension'); + + // Look for invalid modules. + /** @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, + '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:', + 'Invalid module', + '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, + '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:', + 'Invalid theme', + 'Invalid themes' + ); + } + } + return $requirements; } diff --git a/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php b/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php index 5675cb99bb..814db6ea0a 100644 --- a/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php +++ b/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php @@ -2,19 +2,7 @@ namespace Drupal\Tests\system\Functional\Update; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\DependencyInjection\ServiceProviderInterface; -use Drupal\Core\Extension\ExtensionDiscovery; -use Drupal\Core\Extension\InfoParser; -use Drupal\Core\Extension\InfoParserInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Extension\ThemeEngineExtensionList; -use Drupal\Core\Extension\ThemeExtensionList; -use Drupal\Core\State\StateInterface; use Drupal\FunctionalTests\Update\UpdatePathTestBase; -use org\bovigo\vfs\vfsStream; /** * Tests the upgrade path for introducing the Stable base theme. @@ -24,14 +12,7 @@ * @group system * @group legacy */ -class StableBaseThemeUpdateTest extends UpdatePathTestBase implements ServiceProviderInterface { - - /** - * The theme handler. - * - * @var \Drupal\Core\Extension\ThemeHandlerInterface - */ - protected $themeHandler; +class StableBaseThemeUpdateTest extends UpdatePathTestBase { /** * {@inheritdoc} @@ -48,43 +29,17 @@ protected function setDatabaseDumpFiles() { ]; } - /** - * {@inheritdoc} - */ - public function register(ContainerBuilder $container) { - $container->getDefinition('extension.list.theme') - ->setClass(VfsThemeExtensionList::class); - } - /** * {@inheritdoc} */ protected function prepareEnvironment() { parent::prepareEnvironment(); - $GLOBALS['conf']['container_service_providers']['test'] = $this; - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->themeHandler = $this->container->get('theme_handler'); - $this->themeHandler->refreshInfo(); - - $vfs_root = vfsStream::setup('core'); - vfsStream::create([ - 'themes' => [ - 'test_stable' => [ - 'test_stable.info.yml' => file_get_contents(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.info.yml'), - 'test_stable.theme' => file_get_contents(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.theme'), - ], - 'stable' => [ - 'stable.info.yml' => file_get_contents(DRUPAL_ROOT . '/core/themes/stable/stable.info.yml'), - 'stable.theme' => file_get_contents(DRUPAL_ROOT . '/core/themes/stable/stable.theme'), - ], - ], - ], $vfs_root); + // Make the test theme without a base_theme available to the extension + // listing service. + mkdir($this->siteDirectory . '/themes'); + mkdir($this->siteDirectory . '/themes/test_stable'); + copy(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.info.yml', $this->siteDirectory . '/themes/test_stable/test_stable.info.yml'); + copy(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.theme', $this->siteDirectory . '/themes/test_stable/test_stable.theme'); } /** @@ -93,52 +48,12 @@ protected function setUp() { * @expectedDeprecation There is no `base theme` property specified in the test_stable.info.yml file. The optionality of the `base theme` property is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. All Drupal 8 themes must add `base theme: stable` to their *.info.yml file for them to continue to work as-is in future versions of Drupal. Drupal 9 requires the `base theme` property to be specified. See https://www.drupal.org/node/3066038 */ public function testUpdateHookN() { - $this->assertTrue($this->themeHandler->themeExists('test_stable')); - $this->assertFalse($this->themeHandler->themeExists('stable')); + $this->assertTrue(\Drupal::service('theme_handler')->themeExists('test_stable')); + $this->assertFalse(\Drupal::service('theme_handler')->themeExists('stable')); $this->runUpdates(); - // Refresh the theme handler now that Stable has been installed. - $this->themeHandler->refreshInfo(); - $this->assertTrue($this->themeHandler->themeExists('stable')); - } - -} - -class VfsThemeExtensionList extends ThemeExtensionList { - - /** - * The extension discovery for this extension list. - * - * @var \Drupal\Core\Extension\ExtensionDiscovery - */ - protected $extensionDiscovery; - - /** - * {@inheritdoc} - */ - public function __construct(string $root, string $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ThemeEngineExtensionList $engine_list, $install_profile) { - parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $config_factory, $engine_list, $install_profile); - $this->extensionDiscovery = new ExtensionDiscovery('vfs://core'); - $this->infoParser = new VfsInfoParser('vfs:/'); - } - - /** - * {@inheritdoc} - */ - public function getExtensionDiscovery() { - return $this->extensionDiscovery; - } - -} - -class VfsInfoParser extends InfoParser { - - /** - * {@inheritdoc} - */ - public function parse($filename) { - return parent::parse("vfs://core/$filename"); + $this->assertTrue(\Drupal::service('theme_handler')->themeExists('stable')); } } diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php index 1a5cbae18f..cfa67b6971 100644 --- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php +++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\system\Functional\UpdateSystem; +use Drupal\Component\Serialization\Yaml; use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\BrowserTestBase; @@ -16,6 +17,8 @@ class UpdateScriptTest extends BrowserTestBase { use RequirementsPageTrait; + const HANDBOOK_MESSAGE = 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.'; + /** * Modules to enable. * @@ -166,6 +169,255 @@ public function testRequirements() { $this->assertSession()->responseContains('Update script test requires this module and version. Currently using Node version ' . \Drupal::VERSION); } + /** + * Tests that extension compatibility changes are handled correctly. + * + * @param array $correct_info + * The initial values for info.yml fail. These should compatible with core. + * @param array $breaking_info + * The values to the info.yml that are not compatible with core. + * @param string $expected_error + * The expected error. + * + * @dataProvider providerExtensionCompatibilityChange + */ + public function testExtensionCompatibilityChange(array $correct_info, array $breaking_info, $expected_error) { + $extension_type = $correct_info['type']; + $assert_session = $this->assertSession(); + $this->drupalLogin( + $this->drupalCreateUser( + [ + 'administer software updates', + $extension_type === 'module' ? 'administer modules' : 'administer themes', + ] + ) + ); + $extension_machine_name = "changing_extension"; + $extension_name = "$extension_machine_name name"; + $base_info = ['name' => $extension_name]; + if ($extension_type === 'theme') { + $base_info['base theme'] = FALSE; + } + $folder_path = \Drupal::service('site.path') . "/{$extension_type}s/$extension_machine_name"; + $file_path = "$folder_path/$extension_machine_name.info.yml"; + mkdir($folder_path, 0777, TRUE); + file_put_contents($file_path, Yaml::encode($base_info + $correct_info)); + $this->enableExtension($extension_type, $extension_machine_name, $extension_name); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + + // If there are no requirements warnings or errors, we expect to be able to + // go through the update process uninterrupted. + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $assert_session->pageTextNotContains($expected_error); + $assert_session->pageTextNotContains($extension_name); + $assert_session->pageTextNotContains(static::HANDBOOK_MESSAGE); + $this->updateRequirementsProblem(); + $this->clickLink(t('Continue')); + $assert_session->pageTextContains('No pending updates.'); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + + // Change the values in the info.yml and confirm updating is not possible. + file_put_contents($file_path, Yaml::encode($base_info + $breaking_info)); + // Reload the update page to ensure the extension with the breaking values + // has not been uninstalled or otherwise affected. + for ($reload = 0; $reload <= 1; $reload++) { + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $assert_session->pageTextContains($expected_error); + $assert_session->pageTextContains($extension_name); + $assert_session->pageTextContains(static::HANDBOOK_MESSAGE); + $assert_session->linkNotExists('Continue'); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + } + + // Fix the values in the info.yml file and confirm updating is possible + // again. + file_put_contents($file_path, Yaml::encode($base_info + $correct_info)); + // If there are no requirements warnings or errors, we expect to be able to + // go through the update process uninterrupted. + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $assert_session->pageTextNotContains($expected_error); + $assert_session->pageTextNotContains($extension_name); + $assert_session->pageTextNotContains(static::HANDBOOK_MESSAGE); + $this->updateRequirementsProblem(); + $this->clickLink(t('Continue')); + $assert_session->pageTextContains('No pending updates.'); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + } + + /** + * Date provider for testExtensionCompatibilityChange(). + */ + public function providerExtensionCompatibilityChange() { + $incompatible_module_message = "The following module is installed, but it is incompatible with Drupal " . \Drupal::VERSION; + $incompatible_theme_message = "The following theme is installed, but it is incompatible with Drupal " . \Drupal::VERSION; + return [ + 'module: core version change' => [ + [ + 'core' => '8.x', + 'type' => 'module', + ], + [ + 'core' => '7.x', + 'type' => 'module', + ], + $incompatible_module_message, + ], + 'module: core to core_version_requirement' => [ + [ + 'core' => '8.x', + 'type' => 'module', + ], + [ + 'core_version_requirement' => '^7', + 'type' => 'module', + ], + $incompatible_module_message, + ], + 'theme: core version change' => [ + [ + 'core' => '8.x', + 'type' => 'theme', + ], + [ + 'core' => '7.x', + 'type' => 'theme', + ], + $incompatible_theme_message, + ], + 'theme: core to core_version_requirement' => [ + [ + 'core' => '8.x', + 'type' => 'theme', + ], + [ + 'core_version_requirement' => '^7', + 'type' => 'theme', + ], + $incompatible_theme_message, + ], + 'module: php requirement' => [ + [ + 'core_version_requirement' => '*', + 'type' => 'module', + 'php' => 1, + ], + [ + 'core_version_requirement' => '*', + 'type' => 'module', + 'php' => 1000000000, + ], + 'The following module is installed, but it is incompatible with PHP ' . phpversion(), + ], + 'theme: php requirement' => [ + [ + 'core_version_requirement' => '*', + 'type' => 'theme', + 'php' => 1, + ], + [ + 'core_version_requirement' => '*', + 'type' => 'theme', + 'php' => 1000000000, + ], + 'The following theme is installed, but it is incompatible with PHP ' . phpversion(), + ], + ]; + } + + /** + * Tests that a missing extension prevent updates. + * + * @param string $extension_type + * The extension type, either 'module' or 'theme'. + * + * @dataProvider providerMissingExtension + */ + public function testMissingExtension($extension_type) { + $assert_session = $this->assertSession(); + $this->drupalLogin( + $this->drupalCreateUser( + [ + 'administer software updates', + $extension_type === 'module' ? 'administer modules' : 'administer themes', + ] + ) + ); + $extension_machine_name = "disappearing_$extension_type"; + $extension_name = 'The magically disappearing extension'; + $extension_info = [ + 'name' => $extension_name, + 'type' => $extension_type, + 'core' => '8.x', + ]; + if ($extension_type === 'theme') { + $extension_info['base theme'] = FALSE; + } + $folder_path = \Drupal::service('site.path') . "/{$extension_type}s/$extension_machine_name"; + $file_path = "$folder_path/$extension_machine_name.info.yml"; + mkdir($folder_path, 0777, TRUE); + file_put_contents($file_path, Yaml::encode($extension_info)); + $this->enableExtension($extension_type, $extension_machine_name, $extension_name); + + // If there are no requirements warnings or errors, we expect to be able to + // go through the update process uninterrupted. + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $this->updateRequirementsProblem(); + $this->clickLink(t('Continue')); + $assert_session->pageTextContains('No pending updates.'); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + + // Delete the info.yml and confirm updates are prevented. + unlink($file_path); + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $assert_session->pageTextContains("The following $extension_type is marked as installed in the core.extension configuration, but it is missing"); + $assert_session->pageTextContains($extension_machine_name); + $assert_session->pageTextContains(static::HANDBOOK_MESSAGE); + $assert_session->linkNotExists('Continue'); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + + // Add the info.yml file back and confirm we are able to go through the + // update process uninterrupted. + file_put_contents($file_path, Yaml::encode($extension_info)); + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $this->updateRequirementsProblem(); + $this->clickLink(t('Continue')); + $assert_session->pageTextContains('No pending updates.'); + $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name); + } + + /** + * Data provider for testMissingExtension(). + */ + public function providerMissingExtension() { + return [ + 'module' => ['module'], + 'theme' => ['theme'], + ]; + } + + /** + * Enables an extension using the UI. + * + * @param string $extension_type + * The extension type. + * @param string $extension_machine_name + * The extension machine name. + * @param string $extension_name + * The extension name. + */ + private function enableExtension($extension_type, $extension_machine_name, $extension_name) { + if ($extension_type === 'module') { + $edit = [ + "modules[$extension_machine_name][enable]" => $extension_machine_name, + ]; + $this->drupalPostForm('admin/modules', $edit, t('Install')); + } + elseif ($extension_type === 'theme') { + $this->drupalGet('admin/appearance'); + $this->click("a[title~=\"$extension_name\"]"); + } + } + /** * Tests the effect of using the update script on the theme system. */ @@ -431,4 +683,17 @@ public function getSystemSchema() { ]; } + /** + * Asserts that an installed extension's config setting is correct. + * + * @param string $extension_type + * The extension type, either 'module' or 'theme'. + * @param string $extension_machine_name + * The extension machine name. + */ + private function assertInstalledExtensionConfig($extension_type, $extension_machine_name) { + $extension_config = $this->container->get('config.factory')->getEditable('core.extension'); + $this->assertSame(0, $extension_config->get("$extension_type.$extension_machine_name")); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php b/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php index 20a45cad5c..7d2da6b6f6 100644 --- a/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php +++ b/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php @@ -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.2 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'); diff --git a/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php b/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php index ad188a4ed3..bf977ee259 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php @@ -206,9 +206,75 @@ 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 array $additional_settings + * The additional settings to add to extensions info.yml file. + * * @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_settings = []) { vfsStream::setup('drupal_root'); $folders = ['example' => []]; @@ -217,7 +283,7 @@ protected function setupTestExtensionList($extension_names = ['test_name']) { 'name' => 'test name', 'type' => 'test_extension', 'core' => '8.x', - ]); + ] + $additional_settings); } vfsStream::create($folders); foreach ($extension_names as $extension_name) {