diff --git a/core/modules/system/css/system.admin.css b/core/modules/system/css/system.admin.css
index 0165720fede0..2577c724b164 100644
--- a/core/modules/system/css/system.admin.css
+++ b/core/modules/system/css/system.admin.css
@@ -195,6 +195,15 @@ small .admin-link:after {
[dir="rtl"] .module-link-configure {
background-position: top 50% right 0;
}
+.module-link--non-stable {
+ padding-left: 18px;
+ background: url(../../../misc/icons/e29700/warning.svg) 0 50% no-repeat; /* LTR */
+}
+[dir="rtl"] .module-link--non-stable {
+ padding-right: 18px;
+ padding-left: 0;
+ background-position: top 50% right 0;
+}
/* Status report. */
.system-status-report__status-title {
diff --git a/core/modules/system/src/Form/ModulesListExperimentalConfirmForm.php b/core/modules/system/src/Form/ModulesListExperimentalConfirmForm.php
deleted file mode 100644
index 2f410f278557..000000000000
--- a/core/modules/system/src/Form/ModulesListExperimentalConfirmForm.php
+++ /dev/null
@@ -1,39 +0,0 @@
-t('Are you sure you wish to enable experimental modules?');
- }
-
- /**
- * {@inheritdoc}
- */
- public function getFormId() {
- return 'system_modules_experimental_confirm_form';
- }
-
- /**
- * {@inheritdoc}
- */
- protected function buildMessageList() {
- $this->messenger()->addWarning($this->t('Experimental modules are provided for testing purposes only. Use at your own risk.', [':url' => 'https://www.drupal.org/core/experimental']));
-
- $items = parent::buildMessageList();
- // Add the list of experimental modules after any other messages.
- $items[] = $this->t('The following modules are experimental: @modules', ['@modules' => implode(', ', array_values($this->modules['experimental']))]);
-
- return $items;
- }
-
-}
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index 57122be5e7b6..7ad68ac551d1 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -15,6 +15,7 @@ use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
+use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\PermissionHandlerInterface;
@@ -249,7 +250,22 @@ class ModulesListForm extends FormBase {
$row['#requires'] = [];
$row['#required_by'] = [];
+ $lifecycle = $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
$row['name']['#markup'] = $module->info['name'];
+ if ($lifecycle !== ExtensionLifecycle::STABLE && !empty($module->info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER])) {
+ $row['name']['#markup'] .= ' ' . Link::fromTextAndUrl('(' . $this->t('@lifecycle', ['@lifecycle' => ucfirst($lifecycle)]) . ')',
+ Url::fromUri($module->info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER], [
+ 'attributes' =>
+ [
+ 'class' => 'module-link--non-stable',
+ 'aria-label' => $this->t('View information on the @lifecycle status of the module @module', [
+ '@lifecycle' => ucfirst($lifecycle),
+ '@module' => $module->info['name'],
+ ]),
+ ],
+ ])
+ )->toString();
+ }
$row['description']['#markup'] = $this->t($module->info['description']);
$row['version']['#markup'] = $module->info['version'];
@@ -390,7 +406,7 @@ class ModulesListForm extends FormBase {
$modules = [
'install' => [],
'dependencies' => [],
- 'experimental' => [],
+ 'non_stable' => [],
];
$data = $this->moduleExtensionList->getList();
@@ -405,10 +421,12 @@ class ModulesListForm extends FormBase {
}
// Selected modules should be installed.
elseif (($checkbox = $form_state->getValue(['modules', $name], FALSE)) && $checkbox['enable']) {
- $modules['install'][$name] = $data[$name]->info['name'];
- // Identify experimental modules.
- if ($data[$name]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
- $modules['experimental'][$name] = $data[$name]->info['name'];
+ $info = $data[$name]->info;
+ $modules['install'][$name] = $info['name'];
+ // Identify non-stable modules.
+ $lifecycle = $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
+ if ($lifecycle !== ExtensionLifecycle::STABLE) {
+ $modules['non_stable'][$name] = $info['name'];
}
}
}
@@ -417,12 +435,14 @@ class ModulesListForm extends FormBase {
foreach ($modules['install'] as $module => $value) {
foreach (array_keys($data[$module]->requires) as $dependency) {
if (!isset($modules['install'][$dependency]) && !$this->moduleHandler->moduleExists($dependency)) {
- $modules['dependencies'][$module][$dependency] = $data[$dependency]->info['name'];
- $modules['install'][$dependency] = $data[$dependency]->info['name'];
+ $dependency_info = $data[$dependency]->info;
+ $modules['dependencies'][$module][$dependency] = $dependency_info['name'];
+ $modules['install'][$dependency] = $dependency_info['name'];
- // Identify experimental modules.
- if ($data[$dependency]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
- $modules['experimental'][$dependency] = $data[$dependency]->info['name'];
+ // Identify non-stable modules.
+ $lifecycle = $dependency_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
+ if ($lifecycle !== ExtensionLifecycle::STABLE) {
+ $modules['non_stable'][$dependency] = $dependency_info['name'];
}
}
}
@@ -436,7 +456,7 @@ class ModulesListForm extends FormBase {
foreach (array_keys($modules['install']) as $module) {
if (!drupal_check_module($module)) {
unset($modules['install'][$module]);
- unset($modules['experimental'][$module]);
+ unset($modules['non_stable'][$module]);
foreach (array_keys($data[$module]->required_by) as $dependent) {
unset($modules['install'][$dependent]);
unset($modules['dependencies'][$dependent]);
@@ -455,9 +475,9 @@ class ModulesListForm extends FormBase {
$modules = $this->buildModuleList($form_state);
// Redirect to a confirmation form if needed.
- if (!empty($modules['experimental']) || !empty($modules['dependencies'])) {
+ if (!empty($modules['non_stable']) || !empty($modules['dependencies'])) {
- $route_name = !empty($modules['experimental']) ? 'system.modules_list_experimental_confirm' : 'system.modules_list_confirm';
+ $route_name = !empty($modules['non_stable']) ? 'system.modules_list_non_stable_confirm' : 'system.modules_list_confirm';
// Write the list of changed module states into a key value store.
$account = $this->currentUser()->id();
$this->keyValueExpirable->setWithExpire($account, $modules, 60);
diff --git a/core/modules/system/src/Form/ModulesListNonStableConfirmForm.php b/core/modules/system/src/Form/ModulesListNonStableConfirmForm.php
new file mode 100644
index 000000000000..e24ec162a640
--- /dev/null
+++ b/core/modules/system/src/Form/ModulesListNonStableConfirmForm.php
@@ -0,0 +1,200 @@
+moduleExtensionList = $moduleExtensionList;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('module_handler'),
+ $container->get('module_installer'),
+ $container->get('keyvalue.expirable')->get('module_list'),
+ $container->get('extension.list.module')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ $hasExperimentalModulesToEnable = !empty($this->groupedModuleInfo[ExtensionLifecycle::EXPERIMENTAL]);
+ $hasDeprecatedModulesToEnable = !empty($this->groupedModuleInfo[ExtensionLifecycle::DEPRECATED]);
+
+ if ($hasExperimentalModulesToEnable && $hasDeprecatedModulesToEnable) {
+ return $this->t('Are you sure you wish to enable experimental and deprecated modules?');
+ }
+
+ if ($hasExperimentalModulesToEnable) {
+ return $this->formatPlural(
+ count($this->groupedModuleInfo[ExtensionLifecycle::EXPERIMENTAL]),
+ 'Are you sure you wish to enable an experimental module?',
+ 'Are you sure you wish to enable experimental modules?'
+ );
+ }
+
+ if ($hasDeprecatedModulesToEnable) {
+ return $this->formatPlural(
+ count($this->groupedModuleInfo[ExtensionLifecycle::DEPRECATED]),
+ 'Are you sure you wish to enable a deprecated module?',
+ 'Are you sure you wish to enable deprecated modules?'
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'system_modules_non_stable_confirm_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildMessageList() {
+ $this->buildNonStableInfo();
+
+ $items = parent::buildMessageList();
+ if (!empty($this->groupedModuleInfo[ExtensionLifecycle::EXPERIMENTAL])) {
+ $this->messenger()->addWarning($this->t('Experimental modules are provided for testing purposes only. Use at your own risk.', [':url' => 'https://www.drupal.org/core/experimental']));
+ // Add the list of experimental modules after any other messages.
+ $items[] = $this->formatPlural(
+ count($this->groupedModuleInfo[ExtensionLifecycle::EXPERIMENTAL]),
+ 'The following module is experimental: @modules.',
+ 'The following modules are experimental: @modules.',
+ ['@modules' => implode(', ', $this->groupedModuleInfo[ExtensionLifecycle::EXPERIMENTAL])]
+ );
+ }
+ if (!empty($this->groupedModuleInfo[ExtensionLifecycle::DEPRECATED])) {
+ $this->messenger()->addWarning($this->buildDeprecatedMessage($this->coreDeprecatedModules, $this->contribDeprecatedModules));
+ $items = array_merge($items, $this->groupedModuleInfo[ExtensionLifecycle::DEPRECATED]);
+ }
+
+ return $items;
+ }
+
+ /**
+ * Builds a message to be displayed to the user enabling deprecated modules.
+ *
+ * @param bool $core_deprecated_modules
+ * TRUE if a core deprecated module is being enabled.
+ * @param bool $contrib_deprecated_modules
+ * TRUE if a contrib deprecated module is being enabled.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+ * The relevant message.
+ */
+ protected function buildDeprecatedMessage(bool $core_deprecated_modules, bool $contrib_deprecated_modules): TranslatableMarkup {
+ if ($contrib_deprecated_modules && $core_deprecated_modules) {
+ return $this->t('Deprecated modules are modules that may be removed from the next major release of Drupal core and the relevant contributed module. Use at your own risk.', [':url' => 'https://www.drupal.org/about/core/policies/core-change-policies/deprecated-modules-and-themes']);
+ }
+ if ($contrib_deprecated_modules) {
+ return $this->t('Deprecated modules are modules that may be removed from the next major release of this project. Use at your own risk.', [':url' => 'https://www.drupal.org/about/core/policies/core-change-policies/deprecated-modules-and-themes']);
+ }
+
+ return $this->t('Deprecated modules are modules that may be removed from the next major release of Drupal core. Use at your own risk.', [':url' => 'https://www.drupal.org/about/core/policies/core-change-policies/deprecated-modules-and-themes']);
+ }
+
+ /**
+ * Sets properties with information about non-stable modules being enabled.
+ */
+ protected function buildNonStableInfo(): void {
+ $non_stable = $this->modules['non_stable'];
+ $data = $this->moduleExtensionList->getList();
+ $grouped = [];
+ $core_deprecated_modules = FALSE;
+ $contrib_deprecated_modules = FALSE;
+ foreach ($non_stable as $machine_name => $name) {
+ $lifecycle = $data[$machine_name]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
+ if ($lifecycle === ExtensionLifecycle::EXPERIMENTAL) {
+ // We just show the extension name if it is experimental.
+ $grouped[$lifecycle][] = $name;
+ continue;
+ }
+ $core_deprecated_modules = $core_deprecated_modules || $data[$machine_name]->origin === 'core';
+ $contrib_deprecated_modules = $contrib_deprecated_modules || $data[$machine_name]->origin !== 'core';
+ // If the extension is deprecated we show links to more information.
+ $grouped[$lifecycle][] = Link::fromTextAndUrl(
+ $this->t('The @name module is deprecated. (more information)', [
+ '@name' => $name,
+ ]),
+ Url::fromUri($data[$machine_name]->info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER], [
+ 'attributes' =>
+ [
+ 'aria-label' => ' ' . $this->t('about the status of the @name module', [
+ '@name' => $name,
+ ]),
+ ],
+ ])
+ )->toString();
+ }
+
+ $this->groupedModuleInfo = $grouped;
+ $this->coreDeprecatedModules = $core_deprecated_modules;
+ $this->contribDeprecatedModules = $contrib_deprecated_modules;
+ }
+
+}
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index ffca932ba693..00eddce6b550 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -281,11 +281,11 @@ system.modules_list_confirm:
requirements:
_permission: 'administer modules'
-system.modules_list_experimental_confirm:
- path: '/admin/modules/list/confirm-experimental'
+system.modules_list_non_stable_confirm:
+ path: '/admin/modules/list/confirm-non-stable'
defaults:
- _form: '\Drupal\system\Form\ModulesListExperimentalConfirmForm'
- _title: 'Experimental modules'
+ _form: '\Drupal\system\Form\ModulesListNonStableConfirmForm'
+ _title: 'Non-stable modules'
requirements:
_permission: 'administer modules'
diff --git a/core/modules/system/tests/modules/deprecated_module/deprecated_module.info.yml b/core/modules/system/tests/modules/deprecated_module/deprecated_module.info.yml
new file mode 100644
index 000000000000..991a4a89a82e
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_module/deprecated_module.info.yml
@@ -0,0 +1,7 @@
+name: Deprecated module
+type: module
+description: 'Deprecated module'
+package: Testing
+version: VERSION
+lifecycle: deprecated
+lifecycle_link: 'http://example.com/deprecated'
diff --git a/core/modules/system/tests/modules/deprecated_module_contrib/deprecated_module_contrib.info.yml b/core/modules/system/tests/modules/deprecated_module_contrib/deprecated_module_contrib.info.yml
new file mode 100644
index 000000000000..5ead0270f224
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_module_contrib/deprecated_module_contrib.info.yml
@@ -0,0 +1,7 @@
+name: Deprecated module contrib
+type: module
+description: 'Deprecated module contrib'
+package: Testing
+version: VERSION
+lifecycle: deprecated
+lifecycle_link: 'http://example.com/deprecated'
diff --git a/core/modules/system/tests/modules/deprecated_module_dependency/deprecated_module_dependency.info.yml b/core/modules/system/tests/modules/deprecated_module_dependency/deprecated_module_dependency.info.yml
new file mode 100644
index 000000000000..a7df141d9d50
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_module_dependency/deprecated_module_dependency.info.yml
@@ -0,0 +1,7 @@
+name: Deprecated module dependency
+type: module
+description: 'Module that depends on a deprecated module'
+package: Testing
+version: VERSION
+dependencies:
+ - drupal:deprecated_module
diff --git a/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.info.yml b/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.info.yml
new file mode 100644
index 000000000000..1ddf3b60b2f0
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.info.yml
@@ -0,0 +1,5 @@
+name: Deprecated module test
+type: module
+description: 'Deprecated module test'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module b/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module
new file mode 100644
index 000000000000..6d4ab17dd97b
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module
@@ -0,0 +1,18 @@
+origin = 'sites/all';
+ }
+}
diff --git a/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php b/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php
index af41b0de5d6d..8955c70640d4 100644
--- a/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php
+++ b/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php
@@ -53,6 +53,10 @@ class ModulesListFormWebTest extends BrowserTestBase {
// module is used because its machine name is different than its human
// readable name.
$this->assertSession()->pageTextContains('dblog');
+
+ // Check that the deprecated module link was rendered correctly.
+ $this->assertSession()->elementExists('xpath', "//a[contains(@aria-label, 'View information on the Deprecated status of the module Deprecated module')]");
+ $this->assertSession()->elementExists('xpath', "//a[contains(@href, 'http://example.com/deprecated')]");
}
/**
diff --git a/core/modules/system/tests/src/Functional/Module/ExperimentalModuleTest.php b/core/modules/system/tests/src/Functional/Module/ExperimentalModuleTest.php
deleted file mode 100644
index 8f34316a3a9e..000000000000
--- a/core/modules/system/tests/src/Functional/Module/ExperimentalModuleTest.php
+++ /dev/null
@@ -1,148 +0,0 @@
-adminUser = $this->drupalCreateUser([
- 'access administration pages',
- 'administer modules',
- ]);
- $this->drupalLogin($this->adminUser);
- }
-
- /**
- * Tests installing experimental modules and dependencies in the UI.
- */
- public function testExperimentalConfirmForm() {
-
- // First, test installing a non-experimental module with no dependencies.
- // There should be no confirmation form and no experimental module warning.
- $edit = [];
- $edit["modules[test_page_test][enable]"] = TRUE;
- $this->drupalGet('admin/modules');
- $this->submitForm($edit, 'Install');
- $this->assertSession()->pageTextContains('Module Test page has been enabled.');
- $this->assertSession()->pageTextNotContains('Experimental modules are provided for testing purposes only.');
-
- // Uninstall the module.
- \Drupal::service('module_installer')->uninstall(['test_page_test']);
-
- // Next, test installing an experimental module with no dependencies.
- // There should be a confirmation form with an experimental warning, but no
- // list of dependencies.
- $edit = [];
- $edit["modules[experimental_module_test][enable]"] = TRUE;
- $this->drupalGet('admin/modules');
- $this->submitForm($edit, 'Install');
-
- // The module should not be enabled and there should be a warning and a
- // list of the experimental modules with only this one.
- $this->assertSession()->pageTextNotContains('Experimental Test has been enabled.');
- $this->assertSession()->pageTextContains('Experimental modules are provided for testing purposes only.');
- $this->assertSession()->pageTextContains('The following modules are experimental: Experimental Test');
-
- // There should be no message about enabling dependencies.
- $this->assertSession()->pageTextNotContains('You must enable');
-
- // Enable the module and confirm that it worked.
- $this->submitForm([], 'Continue');
- $this->assertSession()->pageTextContains('Experimental Test has been enabled.');
-
- // Uninstall the module.
- \Drupal::service('module_installer')->uninstall(['experimental_module_test']);
-
- // Test enabling a module that is not itself experimental, but that depends
- // on an experimental module.
- $edit = [];
- $edit["modules[experimental_module_dependency_test][enable]"] = TRUE;
- $this->drupalGet('admin/modules');
- $this->submitForm($edit, 'Install');
-
- // The module should not be enabled and there should be a warning and a
- // list of the experimental modules with only this one.
- $this->assertSession()->pageTextNotContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
- $this->assertSession()->pageTextContains('Experimental modules are provided for testing purposes only.');
-
- $this->assertSession()->pageTextContains('The following modules are experimental: Experimental Test');
-
- // Ensure the non-experimental module is not listed as experimental.
- $this->assertSession()->pageTextNotContains('The following modules are experimental: Experimental Test, Experimental Dependency Test');
- $this->assertSession()->pageTextNotContains('The following modules are experimental: Experimental Dependency Test');
-
- // There should be a message about enabling dependencies.
- $this->assertSession()->pageTextContains('You must enable the Experimental Test module to install Experimental Dependency Test');
-
- // Enable the module and confirm that it worked.
- $this->submitForm([], 'Continue');
- $this->assertSession()->pageTextContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
-
- // Uninstall the modules.
- \Drupal::service('module_installer')->uninstall(['experimental_module_test', 'experimental_module_dependency_test']);
-
- // Finally, check both the module and its experimental dependency. There is
- // still a warning about experimental modules, but no message about
- // dependencies, since the user specifically enabled the dependency.
- $edit = [];
- $edit["modules[experimental_module_test][enable]"] = TRUE;
- $edit["modules[experimental_module_dependency_test][enable]"] = TRUE;
- $this->drupalGet('admin/modules');
- $this->submitForm($edit, 'Install');
-
- // The module should not be enabled and there should be a warning and a
- // list of the experimental modules with only this one.
- $this->assertSession()->pageTextNotContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
- $this->assertSession()->pageTextContains('Experimental modules are provided for testing purposes only.');
-
- $this->assertSession()->pageTextContains('The following modules are experimental: Experimental Test');
-
- // Ensure the non-experimental module is not listed as experimental.
- $this->assertSession()->pageTextNotContains('The following modules are experimental: Experimental Dependency Test, Experimental Test');
- $this->assertSession()->pageTextNotContains('The following modules are experimental: Experimental Dependency Test');
-
- // There should be no message about enabling dependencies.
- $this->assertSession()->pageTextNotContains('You must enable');
-
- // Enable the module and confirm that it worked.
- $this->submitForm([], 'Continue');
- $this->assertSession()->pageTextContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
-
- // Try to enable an experimental module that can not be due to
- // hook_requirements().
- \Drupal::state()->set('experimental_module_requirements_test_requirements', TRUE);
- $edit = [];
- $edit["modules[experimental_module_requirements_test][enable]"] = TRUE;
- $this->drupalGet('admin/modules');
- $this->submitForm($edit, 'Install');
- // Verify that if the module can not be installed, we are not taken to the
- // confirm form.
- $this->assertSession()->addressEquals('admin/modules');
- $this->assertSession()->pageTextContains('The Experimental Test Requirements module can not be installed.');
- }
-
-}
diff --git a/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php b/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
index 11030908cc23..f2fca213dae0 100644
--- a/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
+++ b/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
@@ -112,7 +112,7 @@ class InstallUninstallTest extends ModuleTestBase {
// Handle experimental modules, which require a confirmation screen.
if ($lifecycle === ExtensionLifecycle::EXPERIMENTAL) {
- $this->assertSession()->pageTextContains('Are you sure you wish to enable experimental modules?');
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable an experimental module?');
if (count($modules_to_install) > 1) {
// When there are experimental modules, needed dependencies do not
// result in the same page title, but there will be expected text
diff --git a/core/modules/system/tests/src/Functional/Module/NonStableModulesTest.php b/core/modules/system/tests/src/Functional/Module/NonStableModulesTest.php
new file mode 100644
index 000000000000..c90cfd486f49
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Module/NonStableModulesTest.php
@@ -0,0 +1,343 @@
+adminUser = $this->drupalCreateUser([
+ 'access administration pages',
+ 'administer modules',
+ ]);
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests installing experimental modules and dependencies in the UI.
+ */
+ public function testExperimentalConfirmForm(): void {
+ // First, test installing a non-experimental module with no dependencies.
+ // There should be no confirmation form and no experimental module warning.
+ $edit = [];
+ $edit["modules[test_page_test][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+ $this->assertSession()->pageTextContains('Module Test page has been enabled.');
+ $this->assertSession()->pageTextNotContains('Experimental modules are provided for testing purposes only.');
+
+ // There should be no warning about enabling experimental or deprecated
+ // modules, since there's no confirmation form.
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable ');
+
+ // Uninstall the module.
+ \Drupal::service('module_installer')->uninstall(['test_page_test']);
+
+ // Next, test installing an experimental module with no dependencies.
+ // There should be a confirmation form with an experimental warning, but no
+ // list of dependencies.
+ $edit = [];
+ $edit["modules[experimental_module_test][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the experimental modules with only this one.
+ $this->assertSession()->pageTextNotContains('Experimental Test has been enabled.');
+ $this->assertSession()->pageTextContains('Experimental modules are provided for testing purposes only.');
+ $this->assertSession()->pageTextContains('The following module is experimental: Experimental Test');
+
+ // There should be a warning about enabling experimental modules, but no
+ // warnings about deprecated modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable an experimental module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable a deprecated module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental and deprecated modules?');
+
+ // There should be no message about enabling dependencies.
+ $this->assertSession()->pageTextNotContains('You must enable');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $this->assertSession()->pageTextContains('Experimental Test has been enabled.');
+
+ // Uninstall the module.
+ \Drupal::service('module_installer')->uninstall(['experimental_module_test']);
+
+ // Test enabling a module that is not itself experimental, but that depends
+ // on an experimental module.
+ $edit = [];
+ $edit["modules[experimental_module_dependency_test][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the experimental modules with only this one.
+ $this->assertSession()->pageTextNotContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
+ $this->assertSession()->pageTextContains('Experimental modules are provided for testing purposes only.');
+ $this->assertSession()->pageTextContains('The following module is experimental: Experimental Test');
+
+ // There should be a warning about enabling experimental modules, but no
+ // warnings about deprecated modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable an experimental module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable a deprecated module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental and deprecated modules?');
+
+ // Ensure the non-experimental module is not listed as experimental.
+ $this->assertSession()->pageTextNotContains('The following modules are experimental: Experimental Test, Experimental Dependency Test');
+ $this->assertSession()->pageTextNotContains('The following module is experimental: Experimental Dependency Test');
+
+ // There should be a message about enabling dependencies.
+ $this->assertSession()->pageTextContains('You must enable the Experimental Test module to install Experimental Dependency Test');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $this->assertSession()->pageTextContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
+
+ // Uninstall the modules.
+ \Drupal::service('module_installer')->uninstall([
+ 'experimental_module_test',
+ 'experimental_module_dependency_test',
+ ]);
+
+ // Finally, check both the module and its experimental dependency. There is
+ // still a warning about experimental modules, but no message about
+ // dependencies, since the user specifically enabled the dependency.
+ $edit = [];
+ $edit["modules[experimental_module_test][enable]"] = TRUE;
+ $edit["modules[experimental_module_dependency_test][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the experimental modules with only this one.
+ $this->assertSession()->pageTextNotContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
+ $this->assertSession()->pageTextContains('Experimental modules are provided for testing purposes only.');
+ $this->assertSession()->pageTextContains('The following module is experimental: Experimental Test');
+
+ // There should be a warning about enabling experimental modules, but no
+ // warnings about deprecated modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable an experimental module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable a deprecated module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental and deprecated modules?');
+
+ // Ensure the non-experimental module is not listed as experimental.
+ $this->assertSession()->pageTextNotContains('The following modules are experimental: Experimental Dependency Test, Experimental Test');
+ $this->assertSession()->pageTextNotContains('The following module is experimental: Experimental Dependency Test');
+
+ // There should be no message about enabling dependencies.
+ $this->assertSession()->pageTextNotContains('You must enable');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $this->assertSession()->pageTextContains('2 modules have been enabled: Experimental Dependency Test, Experimental Test');
+
+ // Try to enable an experimental module that can not be due to
+ // hook_requirements().
+ \Drupal::state()->set('experimental_module_requirements_test_requirements', TRUE);
+ $edit = [];
+ $edit["modules[experimental_module_requirements_test][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // Verify that if the module can not be installed, we are not taken to the
+ // confirm form.
+ $this->assertSession()->addressEquals('admin/modules');
+ $this->assertSession()->pageTextContains('The Experimental Test Requirements module can not be installed.');
+ }
+
+ /**
+ * Tests installing deprecated modules and dependencies in the UI.
+ */
+ public function testDeprecatedConfirmForm(): void {
+ // Test installing a deprecated module with no dependencies. There should be
+ // a confirmation form with a deprecated warning, but no list of
+ // dependencies.
+ $edit = [];
+ $edit["modules[deprecated_module][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the deprecated modules with only this one.
+ $assert = $this->assertSession();
+ $assert->pageTextNotContains('Deprecated module has been enabled.');
+ $assert->pageTextContains('Deprecated modules are modules that may be removed from the next major release of Drupal core. Use at your own risk.');
+ $assert->pageTextContains('The Deprecated module module is deprecated');
+ $more_information_link = $assert->elementExists('named', [
+ 'link',
+ 'The Deprecated module module is deprecated. (more information)',
+ ]);
+ $this->assertEquals('http://example.com/deprecated', $more_information_link->getAttribute('href'));
+
+ // There should be a warning about enabling deprecated modules, but no
+ // warnings about experimental modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable a deprecated module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable an experimental module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental and deprecated modules?');
+
+ // There should be no message about enabling dependencies.
+ $assert->pageTextNotContains('You must enable');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $assert->pageTextContains('Deprecated module has been enabled.');
+
+ // Uninstall the module.
+ \Drupal::service('module_installer')->uninstall(['deprecated_module']);
+
+ // Test enabling a module that is not itself deprecated, but that depends on
+ // a deprecated module.
+ $edit = [];
+ $edit["modules[deprecated_module_dependency][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the deprecated modules with only this one.
+ $assert->pageTextNotContains('2 modules have been enabled: Deprecated module dependency, Deprecated module');
+ $assert->pageTextContains('Deprecated modules are modules that may be removed from the next major release of Drupal core. Use at your own risk.');
+ $assert->pageTextContains('The Deprecated module module is deprecated');
+
+ // There should be a warning about enabling deprecated modules, but no
+ // warnings about experimental modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable a deprecated module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable an experimental module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental and deprecated modules?');
+
+ // Ensure the non-deprecated module is not listed as deprecated.
+ $assert->pageTextNotContains('The Deprecated module dependency module is deprecated');
+
+ // There should be a message about enabling dependencies.
+ $assert->pageTextContains('You must enable the Deprecated module module to install Deprecated module dependency');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $assert->pageTextContains('2 modules have been enabled: Deprecated module dependency, Deprecated module');
+
+ // Uninstall the modules.
+ \Drupal::service('module_installer')->uninstall([
+ 'deprecated_module',
+ 'deprecated_module_dependency',
+ ]);
+
+ // Finally, check both the module and its deprecated dependency. There is
+ // still a warning about deprecated modules, but no message about
+ // dependencies, since the user specifically enabled the dependency.
+ $edit = [];
+ $edit["modules[deprecated_module_dependency][enable]"] = TRUE;
+ $edit["modules[deprecated_module][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the deprecated modules with only this one.
+ $assert->pageTextNotContains('2 modules have been enabled: Deprecated module dependency, Deprecated module');
+ $assert->pageTextContains('Deprecated modules are modules that may be removed from the next major release of Drupal core. Use at your own risk.');
+ $assert->pageTextContains('The Deprecated module module is deprecated');
+
+ // There should be a warning about enabling deprecated modules, but no
+ // warnings about experimental modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable a deprecated module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable an experimental module?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental and deprecated modules?');
+
+ // Ensure the non-deprecated module is not listed as deprecated.
+ $assert->pageTextNotContains('The Deprecated module dependency module is deprecated');
+
+ // There should be no message about enabling dependencies.
+ $assert->pageTextNotContains('You must enable');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $assert->pageTextContains('2 modules have been enabled: Deprecated module, Deprecated module dependency');
+
+ $this->drupalGet('admin/modules');
+ $this->submitForm(["modules[deprecated_module_contrib][enable]" => TRUE], 'Install');
+ $assert->pageTextContains('Deprecated modules are modules that may be removed from the next major release of this project. Use at your own risk.');
+
+ \Drupal::service('module_installer')->uninstall([
+ 'deprecated_module',
+ 'deprecated_module_dependency',
+ ]);
+ $this->drupalGet('admin/modules');
+ $this->submitForm([
+ "modules[deprecated_module_contrib][enable]" => TRUE,
+ "modules[deprecated_module][enable]" => TRUE,
+ ], 'Install');
+ $assert->pageTextContains('Deprecated modules are modules that may be removed from the next major release of Drupal core and the relevant contributed module. Use at your own risk.');
+ }
+
+ /**
+ * Tests installing deprecated and experimental modules at the same time.
+ */
+ public function testDeprecatedAndExperimentalConfirmForm(): void {
+ $edit = [];
+ $edit["modules[deprecated_module][enable]"] = TRUE;
+ $edit["modules[experimental_module_test][enable]"] = TRUE;
+ $this->drupalGet('admin/modules');
+ $this->submitForm($edit, 'Install');
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the deprecated modules with only this one.
+ $assert = $this->assertSession();
+ $assert->pageTextNotContains('Deprecated module has been enabled.');
+ $assert->pageTextContains('Deprecated modules are modules that may be removed from the next major release of Drupal core. Use at your own risk.');
+ $assert->pageTextContains('The Deprecated module module is deprecated');
+ $more_information_link = $assert->elementExists('named', [
+ 'link',
+ 'The Deprecated module module is deprecated. (more information)',
+ ]);
+ $this->assertEquals('http://example.com/deprecated', $more_information_link->getAttribute('href'));
+
+ // The module should not be enabled and there should be a warning and a
+ // list of the experimental modules with only this one.
+ $assert->pageTextNotContains('Experimental Test has been enabled.');
+ $assert->pageTextContains('Experimental modules are provided for testing purposes only.');
+ $assert->pageTextContains('The following module is experimental: Experimental Test');
+
+ // There should be a warning about enabling experimental and deprecated
+ // modules, but no warnings about solitary experimental or deprecated
+ // modules.
+ $this->assertSession()->pageTextContains('Are you sure you wish to enable experimental and deprecated modules?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable experimental modules?');
+ $this->assertSession()->pageTextNotContains('Are you sure you wish to enable deprecated modules?');
+
+ // There should be no message about enabling dependencies.
+ $assert->pageTextNotContains('You must enable');
+
+ // Enable the module and confirm that it worked.
+ $this->submitForm([], 'Continue');
+ $assert->pageTextContains('2 modules have been enabled: Deprecated module, Experimental Test.');
+ }
+
+}
diff --git a/core/themes/stable/css/system/system.admin.css b/core/themes/stable/css/system/system.admin.css
index 4a3ddb11698d..7bdd79b5552e 100644
--- a/core/themes/stable/css/system/system.admin.css
+++ b/core/themes/stable/css/system/system.admin.css
@@ -199,6 +199,15 @@ small .admin-link:after {
[dir="rtl"] .module-link-configure {
background-position: top 50% right 0;
}
+.module-link--non-stable {
+ padding-left: 18px;
+ background: url(../../../../misc/icons/e29700/warning.svg) 0 50% no-repeat; /* LTR */
+}
+[dir="rtl"] .module-link--non-stable {
+ padding-right: 18px;
+ padding-left: 0;
+ background-position: top 50% right 0;
+}
/* Status report. */
.system-status-report__status-title {
diff --git a/core/themes/stable9/css/system/system.admin.css b/core/themes/stable9/css/system/system.admin.css
index 3399d0e8d5f0..e4b0e61b9788 100644
--- a/core/themes/stable9/css/system/system.admin.css
+++ b/core/themes/stable9/css/system/system.admin.css
@@ -199,6 +199,15 @@ small .admin-link:after {
[dir="rtl"] .module-link-configure {
background-position: top 50% right 0;
}
+.module-link--non-stable {
+ padding-left: 18px;
+ background: url(../../../../misc/icons/e29700/warning.svg) 0 50% no-repeat; /* LTR */
+}
+[dir="rtl"] .module-link--non-stable {
+ padding-right: 18px;
+ padding-left: 0;
+ background-position: top 50% right 0;
+}
/* Status report. */
.system-status-report__status-title {