Commit ed951b05 authored by catch's avatar catch
Browse files

Issue #3215043 by Spokje, larowlan, quietone, dww, srilakshmier, paulocs,...

Issue #3215043 by Spokje, larowlan, quietone, dww, srilakshmier, paulocs, yogeshmpawar, catch, Gábor Hojtsy, benjifisher, AaronMcHale, phenaproxima, kim.pepper, fubarhouse: Indicate the non-stable statuses in admin/modules page

(cherry picked from commit 24893484)
parent ab0ec7c0
......@@ -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 {
......
<?php
namespace Drupal\system\Form;
/**
* Builds a confirmation form for enabling experimental modules.
*
* @internal
*/
class ModulesListExperimentalConfirmForm extends ModulesListConfirmForm {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->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('<a href=":url">Experimental modules</a> 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;
}
}
......@@ -15,6 +15,7 @@
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 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
$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 @@ protected function buildModuleList(FormStateInterface $form_state) {
$modules = [
'install' => [],
'dependencies' => [],
'experimental' => [],
'non_stable' => [],
];
$data = $this->moduleExtensionList->getList();
......@@ -405,10 +421,12 @@ protected function buildModuleList(FormStateInterface $form_state) {
}
// 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 @@ protected function buildModuleList(FormStateInterface $form_state) {
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'];
// Identify experimental modules.
if ($data[$dependency]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$modules['experimental'][$dependency] = $data[$dependency]->info['name'];
$dependency_info = $data[$dependency]->info;
$modules['dependencies'][$module][$dependency] = $dependency_info['name'];
$modules['install'][$dependency] = $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 @@ protected function buildModuleList(FormStateInterface $form_state) {
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 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$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);
......
<?php
namespace Drupal\system\Form;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Drupal\Core\Link;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds a confirmation form for enabling non-stable modules.
*
* @internal
*/
class ModulesListNonStableConfirmForm extends ModulesListConfirmForm {
/**
* Module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* An array of module names to be enabled, keyed by lifecycle.
*
* @var array
*/
protected array $groupedModuleInfo;
/**
* Boolean indicating a core deprecated module is being enabled.
*
* @var bool
*/
protected bool $coreDeprecatedModules;
/**
* Boolean indicating a contrib deprecated module is being enabled.
*
* @var bool
*/
protected bool $contribDeprecatedModules;
/**
* Constructs a new ModulesListNonStableConfirmForm.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
* The module installer.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
* The module extension list.
*/
public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $moduleExtensionList) {
parent::__construct($module_handler, $module_installer, $key_value_expirable);
$this->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('<a href=":url">Experimental modules</a> 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('<a href=":url">Deprecated modules</a> 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('<a href=":url">Deprecated modules</a> 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('<a href=":url">Deprecated modules</a> 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;
}
}
......@@ -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'
......
name: Deprecated module
type: module
description: 'Deprecated module'
package: Testing
version: VERSION
lifecycle: deprecated
lifecycle_link: 'http://example.com/deprecated'
name: Deprecated module contrib
type: module
description: 'Deprecated module contrib'
package: Testing
version: VERSION
lifecycle: deprecated
lifecycle_link: 'http://example.com/deprecated'
name: Deprecated module dependency
type: module
description: 'Module that depends on a deprecated module'
package: Testing
version: VERSION
dependencies:
- drupal:deprecated_module
name: Deprecated module test
type: module
description: 'Deprecated module test'
package: Testing
version: VERSION
<?php
/**
* @file
* Deprecated module test module.
*/
use Drupal\Core\Extension\Extension;
/**
* Implements hook_system_info_alter().
*/
function deprecated_module_test_system_info_alter(array &$info, Extension $file, $type) {
// Make the 'deprecated_module_contrib' look like it isn't part of core.
if ($type === 'module' && $info['name'] === 'Deprecated module contrib') {
$file->origin = 'sites/all';
}
}
......@@ -53,6 +53,10 @@ public function testModuleListForm() {
// 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')]");
}
/**
......
......@@ -112,7 +112,7 @@ public function testInstallUninstall() {
// 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
......
......@@ -3,21 +3,28 @@
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\UserInterface;
/**
* Tests the installation of modules.
* Tests the installation of deprecated and experimental modules.
*
* @group Module
*/
class ExperimentalModuleTest extends BrowserTestBase {
class NonStableModulesTest extends BrowserTestBase {
/**
* The admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected UserInterface $adminUser;
/**
* {@inheritdoc}
*/
protected static $modules = [
'deprecated_module_test',
];
/**
* {@inheritdoc}
......@@ -39,8 +46,7 @@ protected function setUp(): void {
/**
* Tests installing experimental modules and dependencies in the UI.
*/
public function testExperimentalConfirmForm() {
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 = [];
......@@ -50,6 +56,10 @@ public function testExperimentalConfirmForm() {
$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']);
......@@ -65,7 +75,13 @@ public function testExperimentalConfirmForm() {
// 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');
$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');
......@@ -88,12 +104,17 @@ public function testExperimentalConfirmForm() {
// 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');
$this->assertSession()->pageTextContains('The following modules are 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 modules are experimental: 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');
......@@ -103,7 +124,10 @@ public function testExperimentalConfirmForm() {
$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']);
\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
......@@ -118,12 +142,17 @@ public function testExperimentalConfirmForm() {
// 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');
$this->assertSession()->pageTextContains('The following modules are 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 modules are experimental: Experimental Dependency 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');
......@@ -139,10 +168,176 @@ public function testExperimentalConfirmForm() {
$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');