diff --git a/core/includes/update.inc b/core/includes/update.inc index 2021a662..d0af324c 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -9,6 +9,7 @@ */ use Drupal\Component\Graph\Graph; +use Drupal\Component\Version\DrupalSemver; use Drupal\Core\Update\UpdateKernel; use Drupal\Core\Utility\Error; @@ -59,7 +60,7 @@ function update_check_incompatibility($name, $type = 'module') { } if (!isset($file) || !isset($file->info['core']) - || $file->info['core'] != \Drupal::CORE_COMPATIBILITY + || !DrupalSemver::satisfies(\Drupal::VERSION, $file->info['core']) || version_compare(phpversion(), $file->info['php']) < 0) { return TRUE; } diff --git a/core/lib/Drupal/Component/Version/DrupalSemver.php b/core/lib/Drupal/Component/Version/DrupalSemver.php new file mode 100644 index 00000000..74cdd7be --- /dev/null +++ b/core/lib/Drupal/Component/Version/DrupalSemver.php @@ -0,0 +1,37 @@ +status)) { // Ensure this theme is compatible with this version of core. - $theme->incompatible_core = !isset($theme->info['core']) || ($theme->info['core'] != \DRUPAL::CORE_COMPATIBILITY); + $theme->incompatible_core = !isset($theme->info['core']) || !DrupalSemver::satisfies(\Drupal::VERSION, $theme->info['core']); // Require the 'content' region to make sure the main page // content has a common place in all themes. $theme->incompatible_region = !isset($theme->info['regions']['content']); diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index c2ade11b..5c5b9268 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -3,6 +3,7 @@ namespace Drupal\system\Form; use Drupal\Component\Utility\Unicode; +use Drupal\Component\Version\DrupalSemver; use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Config\UnmetDependenciesException; use Drupal\Core\Access\AccessManagerInterface; @@ -281,10 +282,15 @@ protected function buildRow(array $modules, Extension $module, $distribution) { $reasons = []; // Check the core compatibility. - if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) { + if (!DrupalSemver::satisfies(\Drupal::VERSION, $module->info['core'])) { $compatible = FALSE; $reasons[] = $this->t('This version is not compatible with Drupal @core_version and should be replaced.', [ - '@core_version' => \Drupal::CORE_COMPATIBILITY, + '@core_version' => \Drupal::VERSION, + ]); + $row['#requires']['core'] = $this->t('Drupal Core (@core_requirement) (incompatible with version @core_version)', [ + '@module' => $module->getName(), + '@core_requirement' => $module->info['core'], + '@core_version' => \Drupal::VERSION, ]); } @@ -328,7 +334,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) { } // Disable the checkbox if the dependency is incompatible with this // version of Drupal core. - elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) { + elseif (!DrupalSemver::satisfies(\Drupal::VERSION, $modules[$dependency]->info['core'])) { $row['#requires'][$dependency] = $this->t('@module (incompatible with this version of Drupal core)', [ '@module' => $name, ]); diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 799869bb..1cfc4a24 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -293,7 +293,7 @@ function template_preprocess_system_themes_page(&$variables) { // Make sure to provide feedback on compatibility. $current_theme['incompatible'] = ''; if (!empty($theme->incompatible_core)) { - $current_theme['incompatible'] = t("This theme is not compatible with Drupal @core_version. Check that the .info.yml file contains the correct 'core' value.", ['@core_version' => \Drupal::CORE_COMPATIBILITY]); + $current_theme['incompatible'] = t("This theme is not compatible with Drupal @core_version. Check that the .info.yml file contains a compatible 'core' value.", ['@core_version' => \Drupal::VERSION, '@core_constraint' => $theme->info['core']]); } elseif (!empty($theme->incompatible_region)) { $current_theme['incompatible'] = t("This theme is missing a 'content' region."); diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php new file mode 100644 index 00000000..66336095 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php @@ -0,0 +1,38 @@ +merge('key_value') + ->condition('collection', 'system.schema') + ->condition('name', 'update_test_semver_update_n') + ->fields([ + 'collection' => 'system.schema', + 'name' => 'update_test_semver_update_n', + 'value' => 'i:8000;', + ]) + ->execute(); + +// Update core.extension. +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['update_test_semver_update_n'] = 8000; +$connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); diff --git a/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml b/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml new file mode 100644 index 00000000..8063e099 --- /dev/null +++ b/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml @@ -0,0 +1,6 @@ +name: 'System core ^8 version test' +type: module +description: 'Support module for testing core using semver.' +package: Testing +version: 1.0.0 +core: ^8 diff --git a/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_99x.info.yml b/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_99x.info.yml new file mode 100644 index 00000000..449c61b3 --- /dev/null +++ b/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_99x.info.yml @@ -0,0 +1,6 @@ +name: 'System incompatible core 99.x version test' +type: module +description: 'Support module for testing system core incompatibility.' +package: Testing +version: 1.0.0 +core: 99.x diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module index affc20ea..e90b54a8 100644 --- a/core/modules/system/tests/modules/system_test/system_test.module +++ b/core/modules/system/tests/modules/system_test/system_test.module @@ -62,6 +62,10 @@ function system_test_system_info_alter(&$info, Extension $file, $type) { } } + if (($core_requirement = \Drupal::state()->get('dependency_test.core_version_requirement')) && $file->getName() === 'common_test') { + $info['core'] = $core_requirement; + } + // Make the system_dependencies_test visible by default. if ($file->getName() == 'system_dependencies_test') { $info['hidden'] = FALSE; @@ -71,6 +75,7 @@ function system_test_system_info_alter(&$info, Extension $file, $type) { 'system_incompatible_core_version_dependencies_test', 'system_incompatible_module_version_test', 'system_incompatible_core_version_test', + 'system_incompatible_core_version_test_99x', ])) { $info['hidden'] = FALSE; } diff --git a/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml new file mode 100644 index 00000000..5465a62d --- /dev/null +++ b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml @@ -0,0 +1,6 @@ +name: 'Update test hook_update_n semver' +type: module +description: 'Support module for update testing with core semver value.' +package: Testing +version: VERSION +core: ^8 diff --git a/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install new file mode 100644 index 00000000..d00ff0cf --- /dev/null +++ b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install @@ -0,0 +1,13 @@ +set('update_test_semver_update_n_update_8001', 'Yes, I was run. Thanks for testing!'); +} diff --git a/core/modules/system/tests/src/Functional/Module/DependencyTest.php b/core/modules/system/tests/src/Functional/Module/DependencyTest.php index d2938721..d8e17cf8 100644 --- a/core/modules/system/tests/src/Functional/Module/DependencyTest.php +++ b/core/modules/system/tests/src/Functional/Module/DependencyTest.php @@ -102,6 +102,50 @@ public function testIncompatiblePhpVersionDependency() { $this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.'); } + /** + * Tests enabling modules with different core version specifications. + */ + public function testCoreVersionDependency() { + $assert_session = $this->assertSession(); + list($major, $minor) = explode('.', \Drupal::VERSION); + + $next_minor = $minor + 1; + $next_major = $major + 1; + + // Test the next minor release. + \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$next_minor"); + $this->drupalGet('admin/modules'); + $assert_session->fieldDisabled('modules[system_incompatible_core_version_test_99x][enable]'); + $assert_session->fieldDisabled('modules[common_test][enable]'); + + // Test either current major or the next one. + \Drupal::state()->set('dependency_test.core_version_requirement', "^$major || ^$next_major"); + $this->drupalGet('admin/modules'); + $this->assertFalse($assert_session->elementExists('css', '[name="modules[common_test][enable]"]')->hasAttribute('disabled')); + + // Test either a previous major or the next one. + \Drupal::state()->set('dependency_test.core_version_requirement', "^1 || ^$next_major"); + $this->drupalGet('admin/modules'); + $assert_session->fieldDisabled('modules[common_test][enable]'); + + // Test an invalid major. + \Drupal::state()->set('dependency_test.core_version_requirement', 'this-string-is-invalid'); + $this->drupalGet('admin/modules'); + $assert_session->fieldDisabled('modules[common_test][enable]'); + + // Test the current minor. + \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$minor"); + $this->drupalGet('admin/modules'); + $this->assertFalse($assert_session->elementExists('css', '[name="modules[common_test][enable]"]')->hasAttribute('disabled')); + $this->assertFalse($assert_session->elementExists('css', '[name="modules[system_core_semver_test][enable]"]')->hasAttribute('disabled')); + + // Ensure the modules can actually be installed. + $edit['modules[common_test][enable]'] = 'common_test'; + $edit['modules[system_core_semver_test][enable]'] = 'system_core_semver_test'; + $this->drupalPostForm('admin/modules', $edit, t('Install')); + $this->assertModules(['common_test', 'system_core_semver_test'], TRUE); + } + /** * Tests enabling a module that depends on a module which fails hook_requirements(). */ diff --git a/core/modules/system/tests/src/Functional/System/ThemeTest.php b/core/modules/system/tests/src/Functional/System/ThemeTest.php index 1c69d7a3..a40bf0f4 100644 --- a/core/modules/system/tests/src/Functional/System/ThemeTest.php +++ b/core/modules/system/tests/src/Functional/System/ThemeTest.php @@ -366,8 +366,11 @@ public function testInvalidTheme() { $this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', ['@base_theme' => 'not_real_test_basetheme'])); $this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', ['@base_theme' => 'test_invalid_basetheme'])); $this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', ['@theme_engine' => 'not_real_engine'])); - // Check for the error text of a theme with the wrong core version. - $this->assertText("This theme is not compatible with Drupal 8.x. Check that the .info.yml file contains the correct 'core' value."); + // Check for the error text of a theme with the wrong core version + // using 7.x and ^7. + $incompatible_core_message = 'This theme is not compatible with Drupal ' . \Drupal::VERSION . ". Check that the .info.yml file contains a compatible 'core' value."; + $this->assertThemeIncompatibleText('Theme test with invalid core version', $incompatible_core_message); + $this->assertThemeIncompatibleText('Theme test with invalid semver core version', $incompatible_core_message); // Check for the error text of a theme without a content region. $this->assertText("This theme is missing a 'content' region."); } @@ -436,24 +439,29 @@ public function testUninstallingThemes() { * Tests installing a theme and setting it as default. */ public function testInstallAndSetAsDefault() { - $this->drupalGet('admin/appearance'); - // Bartik is uninstalled in the test profile and has the third "Install and - // set as default" link. - $this->clickLink(t('Install and set as default'), 2); - // Test the confirmation message. - $this->assertText('Bartik is now the default theme.'); - // Make sure Bartik is now set as the default theme in config. - $this->assertEqual($this->config('system.theme')->get('default'), 'bartik'); - - // This checks for a regression. See https://www.drupal.org/node/2498691. - $this->assertNoText('The bartik theme was not found.'); - - $themes = \Drupal::service('theme_handler')->rebuildThemeData(); - $version = $themes['bartik']->info['version']; + $themes = [ + 'bartik' => 'Bartik', + 'test_core_semver' => 'Theme test with semver core version', + ]; + foreach ($themes as $theme_machine_name => $theme_name) { + $this->drupalGet('admin/appearance'); + $this->getSession()->getPage()->findLink("Install $theme_name as default theme")->click(); + // Test the confirmation message. + $this->assertText("$theme_name is now the default theme."); + // Make sure Bartik is now set as the default theme in config. + $this->assertEqual($this->config('system.theme')->get('default'), $theme_machine_name); + + // This checks for a regression. See https://www.drupal.org/node/2498691. + $this->assertNoText("The $theme_machine_name theme was not found."); + + $themes = \Drupal::service('theme_handler')->rebuildThemeData(); + $version = $themes[$theme_machine_name]->info['version']; + + // Confirm Bartik is indicated as the default theme. + $out = $this->getSession()->getPage()->getContent(); + $this->assertTrue((bool) preg_match("/$theme_name " . preg_quote($version) . '\s{2,}\(default theme\)/', $out)); + } - // Confirm Bartik is indicated as the default theme. - $out = $this->getSession()->getPage()->getContent(); - $this->assertTrue((bool) preg_match('/Bartik ' . preg_quote($version) . '\s{2,}\(default theme\)/', $out)); } /** @@ -469,4 +477,16 @@ public function testThemeSettingsNoLogoNoFavicon() { $this->assertText('The configuration options have been saved.'); } + /** + * Asserts that expected incompatibility text is displayed for a theme. + * + * @param string $theme_name + * Theme name to select element on page. This can be a partial name. + * @param $expected_text + * The expected incompatibility text. + */ + private function assertThemeIncompatibleText($theme_name, $expected_text) { + $this->assertSession()->elementExists('css', ".theme-info:contains(\"$theme_name\") .incompatible:contains(\"$expected_text\")"); + } + } diff --git a/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml b/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml new file mode 100644 index 00000000..2ba1eb76 --- /dev/null +++ b/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml @@ -0,0 +1,5 @@ +name: 'Theme test with semver core version' +type: theme +description: 'Test theme which has semver core version.' +version: VERSION +core: ^8 || ^9 diff --git a/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml b/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml new file mode 100644 index 00000000..f349cfc0 --- /dev/null +++ b/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml @@ -0,0 +1,5 @@ +name: 'Theme test with invalid semver core version' +type: theme +description: 'Test theme which has an invalid semver core version.' +version: VERSION +core: ^7 diff --git a/core/modules/update/update.module b/core/modules/update/update.module index c783d7e8..94d433de 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -11,6 +11,7 @@ * ability to install contributed modules and themes via an user interface. */ +use Drupal\Component\Version\DrupalSemver; use Drupal\Core\File\Exception\FileException; use Drupal\Core\Url; use Drupal\Core\Form\FormStateInterface; @@ -696,7 +697,7 @@ function update_verify_update_archive($project, $archive_file, $directory) { $info = \Drupal::service('info_parser')->parse($file->uri); // If the module or theme is incompatible with Drupal core, set an error. - if (empty($info['core']) || $info['core'] != \Drupal::CORE_COMPATIBILITY) { + if (empty($info['core']) || !DrupalSemver::satisfies(\Drupal::VERSION, $info['core'])) { $incompatible[] = !empty($info['name']) ? $info['name'] : t('Unknown'); } else { @@ -716,7 +717,7 @@ function update_verify_update_archive($project, $archive_file, $directory) { '%archive_file contains a version of %names that is not compatible with Drupal @version.', '%archive_file contains versions of modules or themes that are not compatible with Drupal @version: %names', [ - '@version' => \Drupal::CORE_COMPATIBILITY, + '@version' => \Drupal::VERSION, '%archive_file' => $file_system->basename($archive_file), '%names' => implode(', ', $incompatible), ] diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php index 942cb886..504eb96a 100644 --- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php @@ -26,6 +26,7 @@ protected function setDatabaseDumpFiles() { $this->databaseDumpFiles = [ __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz', __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-schema-enabled.php', + __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php', ]; } @@ -99,8 +100,11 @@ public function testUpdateHookN() { // Ensure schema has changed. $this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001); + $this->assertEqual(drupal_get_installed_schema_version('update_test_semver_update_n', TRUE), 8001); // Ensure the index was added for column a. $this->assertTrue($connection->schema()->indexExists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.'); + // Ensure update_test_semver_update_n_update_8001 was run. + $this->assertEquals(\Drupal::state()->get('update_test_semver_update_n_update_8001'), 'Yes, I was run. Thanks for testing!'); } /** diff --git a/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php new file mode 100644 index 00000000..44285eab --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php @@ -0,0 +1,57 @@ +assertSame($result, DrupalSemver::satisfies($version, $constraints)); + } + + public function providerSatisfies() { + $cases = [ + ['8.8.0-dev', '8.x', TRUE], + ['8.8.0', '8.x', TRUE], + ['8.9.9', '8.x', TRUE], + ['8.0.0', '8.x', TRUE], + ['9.0.0', '8.x', FALSE], + ['9.1.0', '8.x', FALSE], + ['8.8.0', '~8', TRUE], + ['8.8.0', '^8', TRUE], + ['8.7.0', '^8.7.6', FALSE], + ['8.7.6', '^8.7.6', TRUE], + ['8.7.8', '^8.7.6', TRUE], + ['9.0.0', '^8.7.6', FALSE], + ['8.0.0', '^8 || ^9', TRUE], + ['9.1.1', '^8 || ^9', TRUE], + ['9.1.1', '^8.7.6 || ^9', TRUE], + ['8.7.8', '^8.7.6 || ^9', TRUE], + ['8.6.8', '^8.7.6 || ^9', FALSE], + ['8.6.8', '^9', FALSE], + ['9.1.1', '^9', TRUE], + ['9.0.0', '9.x', TRUE], + ['8.8.0', '9.x', FALSE], + ['8.8.0', '7.x', FALSE], + ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '9.x', FALSE], + ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '7.x', FALSE], + ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '8.x', FALSE], + ]; + $tests = []; + foreach ($cases as $case) { + $tests[$case[0] . ":" . $case[1]] = $case; + } + return $tests; + } + +}