Newer
Older
Alex Pott
committed
<?php
/**
* @file
* Contains \Drupal\system\Form\ModulesListForm.
*/
namespace Drupal\system\Form;
use Drupal\Component\Utility\String;
Alex Pott
committed
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Access\AccessManagerInterface;
Angie Byron
committed
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\Extension;
Alex Pott
committed
use Drupal\Core\Extension\ModuleHandlerInterface;
Alex Pott
committed
use Drupal\Core\Form\FormBase;
Dries Buytaert
committed
use Drupal\Core\Form\FormStateInterface;
Alex Pott
committed
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
Angie Byron
committed
use Drupal\Core\Render\Element;
Angie Byron
committed
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
Alex Pott
committed
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
Alex Pott
committed
/**
Dries Buytaert
committed
* Provides module installation interface.
Alex Pott
committed
*
* The list of modules gets populated by module.info.yml files, which contain
* each module's name, description, and information about which modules it
* requires. See \Drupal\Core\Extension\InfoParser for info on module.info.yml
Alex Pott
committed
* descriptors.
*/
Alex Pott
committed
class ModulesListForm extends FormBase {
Alex Pott
committed
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
Alex Pott
committed
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The expirable key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueExpirable;
Angie Byron
committed
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The title resolver.
Angie Byron
committed
*
* @var \Drupal\Core\Controller\TitleResolverInterface
Angie Byron
committed
*/
protected $titleResolver;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
Angie Byron
committed
Angie Byron
committed
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
Alex Pott
committed
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
Angie Byron
committed
$container->get('keyvalue.expirable')->get('module_list'),
Angie Byron
committed
$container->get('access_manager'),
$container->get('entity.manager'),
Angie Byron
committed
$container->get('current_user'),
$container->get('current_route_match'),
$container->get('title_resolver'),
$container->get('router.route_provider'),
$container->get('plugin.manager.menu.link')
Alex Pott
committed
);
}
/**
* Constructs a ModulesListForm object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
Angie Byron
committed
* Access manager.
Angie Byron
committed
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
Angie Byron
committed
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
Alex Pott
committed
*/
public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable, AccessManagerInterface $access_manager, EntityManagerInterface $entity_manager, AccountInterface $current_user, RouteMatchInterface $route_match, TitleResolverInterface $title_resolver, RouteProviderInterface $route_provider, MenuLinkManagerInterface $menu_link_manager) {
Alex Pott
committed
$this->moduleHandler = $module_handler;
$this->keyValueExpirable = $key_value_expirable;
Angie Byron
committed
$this->accessManager = $access_manager;
Angie Byron
committed
$this->entityManager = $entity_manager;
$this->currentUser = $current_user;
Angie Byron
committed
$this->routeMatch = $route_match;
$this->titleResolver = $title_resolver;
$this->routeProvider = $route_provider;
$this->menuLinkManager = $menu_link_manager;
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
public function getFormId() {
Alex Pott
committed
return 'system_modules';
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function buildForm(array $form, FormStateInterface $form_state) {
Alex Pott
committed
require_once DRUPAL_ROOT . '/core/includes/install.inc';
$distribution = String::checkPlain(drupal_install_profile_distribution_name());
Alex Pott
committed
// Include system.admin.inc so we can use the sort callbacks.
$this->moduleHandler->loadInclude('system', 'inc', 'system.admin');
$form['filters'] = array(
'#type' => 'container',
'#attributes' => array(
'class' => array('table-filter', 'js-show'),
),
);
$form['filters']['text'] = array(
'#type' => 'search',
Alex Pott
committed
'#title' => $this->t('Search'),
Alex Pott
committed
'#size' => 30,
Alex Pott
committed
'#placeholder' => $this->t('Enter module name'),
Alex Pott
committed
'#attributes' => array(
'class' => array('table-filter-text'),
'data-table' => '#system-modules',
'autocomplete' => 'off',
Alex Pott
committed
'title' => $this->t('Enter a part of the module name or description to filter by.'),
Alex Pott
committed
),
);
// Sort all modules by their names.
$modules = system_rebuild_module_data();
uasort($modules, 'system_sort_modules_by_info_name');
// Iterate over each of the modules.
$form['modules']['#tree'] = TRUE;
foreach ($modules as $filename => $module) {
if (empty($module->info['hidden'])) {
$package = $module->info['package'];
$form['modules'][$package][$filename] = $this->buildRow($modules, $module, $distribution);
}
}
// Add a wrapper around every package.
Angie Byron
committed
foreach (Element::children($form['modules']) as $package) {
Alex Pott
committed
$form['modules'][$package] += array(
'#type' => 'details',
Alex Pott
committed
'#title' => $this->t($package),
Angie Byron
committed
'#open' => TRUE,
Alex Pott
committed
'#theme' => 'system_modules_details',
'#header' => array(
array('data' => $this->t('Installed'), 'class' => array('checkbox', 'visually-hidden')),
array('data' => $this->t('Name'), 'class' => array('name', 'visually-hidden')),
array('data' => $this->t('Description'), 'class' => array('description', 'visually-hidden', RESPONSIVE_PRIORITY_LOW)),
Alex Pott
committed
),
catch
committed
'#attributes' => array('class' => array('package-listing')),
Alex Pott
committed
// Ensure that the "Core" package comes first.
'#weight' => $package == 'Core' ? -10 : NULL,
);
}
// If testing modules are shown, collapse the corresponding package by
// default.
if (isset($form['modules']['Testing'])) {
$form['modules']['Testing']['#open'] = FALSE;
}
Alex Pott
committed
// Lastly, sort all packages by title.
uasort($form['modules'], array('\Drupal\Component\Utility\SortArray', 'sortByTitleProperty'));
Alex Pott
committed
$form['#attached']['library'][] = 'system/drupal.system.modules';
Alex Pott
committed
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
Alex Pott
committed
'#value' => $this->t('Save configuration'),
Alex Pott
committed
);
return $form;
}
/**
* Builds a table row for the system modules page.
*
* @param array $modules
* The list existing modules.
* @param \Drupal\Core\Extension\Extension $module
Alex Pott
committed
* The module for which to build the form row.
* @param $distribution
*
* @return array
* The form row for the given module.
*/
protected function buildRow(array $modules, Extension $module, $distribution) {
Alex Pott
committed
// Set the basic properties.
$row['#required'] = array();
$row['#requires'] = array();
$row['#required_by'] = array();
$row['name']['#markup'] = $module->info['name'];
Alex Pott
committed
$row['description']['#markup'] = $this->t($module->info['description']);
Alex Pott
committed
$row['version']['#markup'] = $module->info['version'];
// Generate link for module's help page, if there is one.
$row['links']['help'] = array();
if ($this->moduleHandler->moduleExists('help') && $module->status && in_array($module->getName(), $this->moduleHandler->getImplementations('help'))) {
Angie Byron
committed
if ($this->moduleHandler->invoke($module->getName(), 'help', array('help.page.' . $module->getName(), $this->routeMatch))) {
Alex Pott
committed
$row['links']['help'] = array(
'#type' => 'link',
Alex Pott
committed
'#title' => $this->t('Help'),
'#href' => 'admin/help/' . $module->getName(),
Alex Pott
committed
'#options' => array('attributes' => array('class' => array('module-link', 'module-link-help'), 'title' => $this->t('Help'))),
Alex Pott
committed
);
}
}
// Generate link for module's permission, if the user has access to it.
$row['links']['permissions'] = array();
if ($module->status && \Drupal::currentUser()->hasPermission('administer permissions') && in_array($module->getName(), $this->moduleHandler->getImplementations('permission'))) {
Alex Pott
committed
$row['links']['permissions'] = array(
'#type' => 'link',
Alex Pott
committed
'#title' => $this->t('Permissions'),
Alex Pott
committed
'#href' => 'admin/people/permissions',
'#options' => array('fragment' => 'module-' . $module->getName(), 'attributes' => array('class' => array('module-link', 'module-link-permissions'), 'title' => $this->t('Configure permissions'))),
Alex Pott
committed
);
}
// Generate link for module's configuration page, if it has one.
$row['links']['configure'] = array();
if ($module->status && isset($module->info['configure'])) {
$route_parameters = isset($module->info['configure_parameters']) ? $module->info['configure_parameters'] : array();
if ($this->accessManager->checkNamedRoute($module->info['configure'], $route_parameters, $this->currentUser)) {
$links = $this->menuLinkManager->loadLinksByRoute($module->info['configure']);
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = reset($links);
// Most configure links have a corresponding menu link, though some just
// have a route.
if ($link) {
$description = $link->getDescription();
}
else {
$request = new Request();
$request->attributes->set('_route_name', $module->info['configure']);
$route_object = $this->routeProvider->getRouteByName($module->info['configure']);
$request->attributes->set('_route', $route_object);
Alex Pott
committed
$request->attributes->add($route_parameters);
$description = $this->titleResolver->getTitle($request, $route_object);
}
Alex Pott
committed
$row['links']['configure'] = array(
'#type' => 'link',
Alex Pott
committed
'#title' => $this->t('Configure'),
Angie Byron
committed
'#route_name' => $module->info['configure'],
'#route_parameters' => $route_parameters,
Angie Byron
committed
'#options' => array(
'attributes' => array(
'class' => array('module-link', 'module-link-configure'),
'title' => $description,
Angie Byron
committed
),
),
Alex Pott
committed
);
}
}
// Present a checkbox for installing and indicating the status of a module.
$row['enable'] = array(
'#type' => 'checkbox',
Dries Buytaert
committed
'#title' => $this->t('Install'),
Alex Pott
committed
'#default_value' => (bool) $module->status,
Dries Buytaert
committed
'#disabled' => (bool) $module->status,
Alex Pott
committed
);
// Disable the checkbox for required modules.
if (!empty($module->info['required'])) {
// Used when displaying modules that are required by the installation profile
$row['enable']['#disabled'] = TRUE;
$row['#required_by'][] = $distribution . (!empty($module->info['explanation']) ? ' ('. $module->info['explanation'] .')' : '');
}
// Check the compatibilities.
$compatible = TRUE;
Angie Byron
committed
// Initialize an empty array of reasons why the module is incompatible. Add
// each reason as a separate element of the array.
$reasons = array();
Alex Pott
committed
// Check the core compatibility.
catch
committed
if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) {
Alex Pott
committed
$compatible = FALSE;
Angie Byron
committed
$reasons[] = $this->t('This version is not compatible with Drupal !core_version and should be replaced.', array(
catch
committed
'!core_version' => \Drupal::CORE_COMPATIBILITY,
Alex Pott
committed
));
}
// Ensure this module is compatible with the currently installed version of PHP.
if (version_compare(phpversion(), $module->info['php']) < 0) {
$compatible = FALSE;
$required = $module->info['php'] . (substr_count($module->info['php'], '.') < 2 ? '.*' : '');
Angie Byron
committed
$reasons[] = $this->t('This module requires PHP version @php_required and is incompatible with PHP version !php_version.', array(
Alex Pott
committed
'@php_required' => $required,
'!php_version' => phpversion(),
));
}
// If this module is not compatible, disable the checkbox.
if (!$compatible) {
Angie Byron
committed
$status = implode(' ', $reasons);
Alex Pott
committed
$row['enable']['#disabled'] = TRUE;
Angie Byron
committed
$row['description']['#markup'] = $status;
$row['#attributes']['class'][] = 'incompatible';
Alex Pott
committed
}
// If this module requires other modules, add them to the array.
foreach ($module->requires as $dependency => $version) {
if (!isset($modules[$dependency])) {
Alex Pott
committed
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', array('@module' => Unicode::ucfirst($dependency)));
Alex Pott
committed
$row['enable']['#disabled'] = TRUE;
}
// Only display visible modules.
elseif (empty($modules[$dependency]->hidden)) {
$name = $modules[$dependency]->info['name'];
// Disable the module's checkbox if it is incompatible with the
// dependency's version.
catch
committed
if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
Alex Pott
committed
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> version @version)', array(
Alex Pott
committed
'@module' => $name . $incompatible_version,
'@version' => $modules[$dependency]->info['version'],
));
$row['enable']['#disabled'] = TRUE;
}
// Disable the checkbox if the dependency is incompatible with this
// version of Drupal core.
catch
committed
elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
Alex Pott
committed
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', array(
Alex Pott
committed
'@module' => $name,
));
$row['enable']['#disabled'] = TRUE;
}
elseif ($modules[$dependency]->status) {
Alex Pott
committed
$row['#requires'][$dependency] = $this->t('@module', array('@module' => $name));
Alex Pott
committed
}
else {
Alex Pott
committed
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $name));
Alex Pott
committed
}
}
}
// If this module is required by other modules, list those, and then make it
// impossible to disable this one.
foreach ($module->required_by as $dependent => $version) {
if (isset($modules[$dependent]) && empty($modules[$dependent]->info['hidden'])) {
if ($modules[$dependent]->status == 1 && $module->status == 1) {
Alex Pott
committed
$row['#required_by'][$dependent] = $this->t('@module', array('@module' => $modules[$dependent]->info['name']));
Alex Pott
committed
$row['enable']['#disabled'] = TRUE;
}
else {
Alex Pott
committed
$row['#required_by'][$dependent] = $this->t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $modules[$dependent]->info['name']));
Alex Pott
committed
}
}
}
return $row;
}
/**
Dries Buytaert
committed
* Helper function for building a list of modules to install.
Alex Pott
committed
*
Dries Buytaert
committed
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
Alex Pott
committed
*
* @return array
Dries Buytaert
committed
* An array of modules to install and their dependencies.
Alex Pott
committed
*/
Alex Pott
committed
protected function buildModuleList(FormStateInterface $form_state) {
Alex Pott
committed
$packages = $form_state->getValue('modules');
Alex Pott
committed
Dries Buytaert
committed
// Build a list of modules to install.
Alex Pott
committed
$modules = array(
Dries Buytaert
committed
'install' => array(),
Alex Pott
committed
'dependencies' => array(),
);
Dries Buytaert
committed
// Required modules have to be installed.
Alex Pott
committed
// @todo This should really not be handled here.
$data = system_rebuild_module_data();
foreach ($data as $name => $module) {
Dries Buytaert
committed
if (!empty($module->required) && !$this->moduleHandler->moduleExists($name)) {
$modules['install'][$name] = $module->info['name'];
Alex Pott
committed
}
}
// First, build a list of all modules that were selected.
foreach ($packages as $items) {
foreach ($items as $name => $checkbox) {
Dries Buytaert
committed
if ($checkbox['enable'] && !$this->moduleHandler->moduleExists($name)) {
$modules['install'][$name] = $data[$name]->info['name'];
Alex Pott
committed
}
}
}
// Add all dependencies to a list.
Dries Buytaert
committed
while (list($module) = each($modules['install'])) {
Alex Pott
committed
foreach (array_keys($data[$module]->requires) as $dependency) {
Dries Buytaert
committed
if (!isset($modules['install'][$dependency]) && !$this->moduleHandler->moduleExists($dependency)) {
Alex Pott
committed
$modules['dependencies'][$module][$dependency] = $data[$dependency]->info['name'];
Dries Buytaert
committed
$modules['install'][$dependency] = $data[$dependency]->info['name'];
Alex Pott
committed
}
}
}
// Make sure the install API is available.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
// Invoke hook_requirements('install'). If failures are detected, make
// sure the dependent modules aren't installed either.
Dries Buytaert
committed
foreach (array_keys($modules['install']) as $module) {
if (!drupal_check_module($module)) {
unset($modules['install'][$module]);
Alex Pott
committed
foreach (array_keys($data[$module]->required_by) as $dependent) {
Dries Buytaert
committed
unset($modules['install'][$dependent]);
Alex Pott
committed
unset($modules['dependencies'][$dependent]);
}
}
}
return $modules;
}
/**
* {@inheritdoc}
*/
Dries Buytaert
committed
public function submitForm(array &$form, FormStateInterface $form_state) {
Dries Buytaert
committed
// Retrieve a list of modules to install and their dependencies.
Alex Pott
committed
$modules = $this->buildModuleList($form_state);
Dries Buytaert
committed
// Check if we have to install any dependencies. If there is one or more
// dependencies that are not installed yet, redirect to the confirmation
// form.
Alex Pott
committed
if (!empty($modules['dependencies']) || !empty($modules['missing'])) {
// Write the list of changed module states into a key value store.
$account = $this->currentUser()->id();
Alex Pott
committed
$this->keyValueExpirable->setWithExpire($account, $modules, 60);
// Redirect to the confirmation form.
Alex Pott
committed
$form_state->setRedirect('system.modules_list_confirm');
Alex Pott
committed
// We can exit here because at least one modules has dependencies
// which we have to prompt the user for in a confirmation form.
return;
}
// Gets list of modules prior to install process.
$before = $this->moduleHandler->getModuleList();
// There seem to be no dependencies that would need approval.
Dries Buytaert
committed
if (!empty($modules['install'])) {
$this->moduleHandler->install(array_keys($modules['install']));
Alex Pott
committed
}
// Gets module list after install process, flushes caches and displays a
// message if there are changes.
if ($before != $this->moduleHandler->getModuleList()) {
drupal_flush_all_caches();
drupal_set_message(t('The configuration options have been saved.'));
}
}
}