Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / core / modules / system / src / Form / ModulesListForm.php
1 <?php
2
3 namespace Drupal\system\Form;
4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\Config\PreExistingConfigException;
7 use Drupal\Core\Config\UnmetDependenciesException;
8 use Drupal\Core\Access\AccessManagerInterface;
9 use Drupal\Core\Extension\Extension;
10 use Drupal\Core\Extension\ModuleHandlerInterface;
11 use Drupal\Core\Extension\ModuleInstallerInterface;
12 use Drupal\Core\Form\FormBase;
13 use Drupal\Core\Form\FormStateInterface;
14 use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
15 use Drupal\Core\Render\Element;
16 use Drupal\Core\Session\AccountInterface;
17 use Drupal\user\PermissionHandlerInterface;
18 use Drupal\Core\Url;
19 use Symfony\Component\DependencyInjection\ContainerInterface;
20
21 /**
22  * Provides module installation interface.
23  *
24  * The list of modules gets populated by module.info.yml files, which contain
25  * each module's name, description, and information about which modules it
26  * requires. See \Drupal\Core\Extension\InfoParser for info on module.info.yml
27  * descriptors.
28  *
29  * @internal
30  */
31 class ModulesListForm extends FormBase {
32
33   /**
34    * The current user.
35    *
36    * @var \Drupal\Core\Session\AccountInterface
37    */
38   protected $currentUser;
39
40   /**
41    * The module handler service.
42    *
43    * @var \Drupal\Core\Extension\ModuleHandlerInterface
44    */
45   protected $moduleHandler;
46
47   /**
48    * The expirable key value store.
49    *
50    * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
51    */
52   protected $keyValueExpirable;
53
54   /**
55    * The module installer.
56    *
57    * @var \Drupal\Core\Extension\ModuleInstallerInterface
58    */
59   protected $moduleInstaller;
60
61   /**
62    * The permission handler.
63    *
64    * @var \Drupal\user\PermissionHandlerInterface
65    */
66   protected $permissionHandler;
67
68   /**
69    * {@inheritdoc}
70    */
71   public static function create(ContainerInterface $container) {
72     return new static(
73       $container->get('module_handler'),
74       $container->get('module_installer'),
75       $container->get('keyvalue.expirable')->get('module_list'),
76       $container->get('access_manager'),
77       $container->get('current_user'),
78       $container->get('user.permissions')
79     );
80   }
81
82   /**
83    * Constructs a ModulesListForm object.
84    *
85    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
86    *   The module handler.
87    * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
88    *   The module installer.
89    * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
90    *   The key value expirable factory.
91    * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
92    *   Access manager.
93    * @param \Drupal\Core\Session\AccountInterface $current_user
94    *   The current user.
95    * @param \Drupal\user\PermissionHandlerInterface $permission_handler
96    *   The permission handler.
97    */
98   public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, AccessManagerInterface $access_manager, AccountInterface $current_user, PermissionHandlerInterface $permission_handler) {
99     $this->moduleHandler = $module_handler;
100     $this->moduleInstaller = $module_installer;
101     $this->keyValueExpirable = $key_value_expirable;
102     $this->accessManager = $access_manager;
103     $this->currentUser = $current_user;
104     $this->permissionHandler = $permission_handler;
105   }
106
107   /**
108    * {@inheritdoc}
109    */
110   public function getFormId() {
111     return 'system_modules';
112   }
113
114   /**
115    * {@inheritdoc}
116    */
117   public function buildForm(array $form, FormStateInterface $form_state) {
118     require_once DRUPAL_ROOT . '/core/includes/install.inc';
119     $distribution = drupal_install_profile_distribution_name();
120
121     // Include system.admin.inc so we can use the sort callbacks.
122     $this->moduleHandler->loadInclude('system', 'inc', 'system.admin');
123
124     $form['filters'] = [
125       '#type' => 'container',
126       '#attributes' => [
127         'class' => ['table-filter', 'js-show'],
128       ],
129     ];
130
131     $form['filters']['text'] = [
132       '#type' => 'search',
133       '#title' => $this->t('Filter modules'),
134       '#title_display' => 'invisible',
135       '#size' => 30,
136       '#placeholder' => $this->t('Filter by name or description'),
137       '#description' => $this->t('Enter a part of the module name or description'),
138       '#attributes' => [
139         'class' => ['table-filter-text'],
140         'data-table' => '#system-modules',
141         'autocomplete' => 'off',
142       ],
143     ];
144
145     // Sort all modules by their names.
146     $modules = system_rebuild_module_data();
147     uasort($modules, 'system_sort_modules_by_info_name');
148
149     // Iterate over each of the modules.
150     $form['modules']['#tree'] = TRUE;
151     foreach ($modules as $filename => $module) {
152       if (empty($module->info['hidden'])) {
153         $package = $module->info['package'];
154         $form['modules'][$package][$filename] = $this->buildRow($modules, $module, $distribution);
155         $form['modules'][$package][$filename]['#parents'] = ['modules', $filename];
156       }
157     }
158
159     // Add a wrapper around every package.
160     foreach (Element::children($form['modules']) as $package) {
161       $form['modules'][$package] += [
162         '#type' => 'details',
163         '#title' => $this->t($package),
164         '#open' => TRUE,
165         '#theme' => 'system_modules_details',
166         '#attributes' => ['class' => ['package-listing']],
167         // Ensure that the "Core" package comes first.
168         '#weight' => $package == 'Core' ? -10 : NULL,
169       ];
170     }
171
172     // If testing modules are shown, collapse the corresponding package by
173     // default.
174     if (isset($form['modules']['Testing'])) {
175       $form['modules']['Testing']['#open'] = FALSE;
176     }
177
178     // Lastly, sort all packages by title.
179     uasort($form['modules'], ['\Drupal\Component\Utility\SortArray', 'sortByTitleProperty']);
180
181     $form['#attached']['library'][] = 'core/drupal.tableresponsive';
182     $form['#attached']['library'][] = 'system/drupal.system.modules';
183     $form['actions'] = ['#type' => 'actions'];
184     $form['actions']['submit'] = [
185       '#type' => 'submit',
186       '#value' => $this->t('Install'),
187       '#button_type' => 'primary',
188     ];
189
190     return $form;
191   }
192
193   /**
194    * Builds a table row for the system modules page.
195    *
196    * @param array $modules
197    *   The list existing modules.
198    * @param \Drupal\Core\Extension\Extension $module
199    *   The module for which to build the form row.
200    * @param $distribution
201    *
202    * @return array
203    *   The form row for the given module.
204    */
205   protected function buildRow(array $modules, Extension $module, $distribution) {
206     // Set the basic properties.
207     $row['#required'] = [];
208     $row['#requires'] = [];
209     $row['#required_by'] = [];
210
211     $row['name']['#markup'] = $module->info['name'];
212     $row['description']['#markup'] = $this->t($module->info['description']);
213     $row['version']['#markup'] = $module->info['version'];
214
215     // Generate link for module's help page. Assume that if a hook_help()
216     // implementation exists then the module provides an overview page, rather
217     // than checking to see if the page exists, which is costly.
218     if ($this->moduleHandler->moduleExists('help') && $module->status && in_array($module->getName(), $this->moduleHandler->getImplementations('help'))) {
219       $row['links']['help'] = [
220         '#type' => 'link',
221         '#title' => $this->t('Help'),
222         '#url' => Url::fromRoute('help.page', ['name' => $module->getName()]),
223         '#options' => ['attributes' => ['class' => ['module-link', 'module-link-help'], 'title' => $this->t('Help')]],
224       ];
225     }
226
227     // Generate link for module's permission, if the user has access to it.
228     if ($module->status && $this->currentUser->hasPermission('administer permissions') && $this->permissionHandler->moduleProvidesPermissions($module->getName())) {
229       $row['links']['permissions'] = [
230         '#type' => 'link',
231         '#title' => $this->t('Permissions'),
232         '#url' => Url::fromRoute('user.admin_permissions'),
233         '#options' => ['fragment' => 'module-' . $module->getName(), 'attributes' => ['class' => ['module-link', 'module-link-permissions'], 'title' => $this->t('Configure permissions')]],
234       ];
235     }
236
237     // Generate link for module's configuration page, if it has one.
238     if ($module->status && isset($module->info['configure'])) {
239       $route_parameters = isset($module->info['configure_parameters']) ? $module->info['configure_parameters'] : [];
240       if ($this->accessManager->checkNamedRoute($module->info['configure'], $route_parameters, $this->currentUser)) {
241         $row['links']['configure'] = [
242           '#type' => 'link',
243           '#title' => $this->t('Configure <span class="visually-hidden">the @module module</span>', ['@module' => $module->info['name']]),
244           '#url' => Url::fromRoute($module->info['configure'], $route_parameters),
245           '#options' => [
246             'attributes' => [
247               'class' => ['module-link', 'module-link-configure'],
248             ],
249           ],
250         ];
251       }
252     }
253
254     // Present a checkbox for installing and indicating the status of a module.
255     $row['enable'] = [
256       '#type' => 'checkbox',
257       '#title' => $this->t('Install'),
258       '#default_value' => (bool) $module->status,
259       '#disabled' => (bool) $module->status,
260     ];
261
262     // Disable the checkbox for required modules.
263     if (!empty($module->info['required'])) {
264       // Used when displaying modules that are required by the installation profile
265       $row['enable']['#disabled'] = TRUE;
266       $row['#required_by'][] = $distribution . (!empty($module->info['explanation']) ? ' (' . $module->info['explanation'] . ')' : '');
267     }
268
269     // Check the compatibilities.
270     $compatible = TRUE;
271
272     // Initialize an empty array of reasons why the module is incompatible. Add
273     // each reason as a separate element of the array.
274     $reasons = [];
275
276     // Check the core compatibility.
277     if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) {
278       $compatible = FALSE;
279       $reasons[] = $this->t('This version is not compatible with Drupal @core_version and should be replaced.', [
280         '@core_version' => \Drupal::CORE_COMPATIBILITY,
281       ]);
282     }
283
284     // Ensure this module is compatible with the currently installed version of PHP.
285     if (version_compare(phpversion(), $module->info['php']) < 0) {
286       $compatible = FALSE;
287       $required = $module->info['php'] . (substr_count($module->info['php'], '.') < 2 ? '.*' : '');
288       $reasons[] = $this->t('This module requires PHP version @php_required and is incompatible with PHP version @php_version.', [
289         '@php_required' => $required,
290         '@php_version' => phpversion(),
291       ]);
292     }
293
294     // If this module is not compatible, disable the checkbox.
295     if (!$compatible) {
296       $status = implode(' ', $reasons);
297       $row['enable']['#disabled'] = TRUE;
298       $row['description']['#markup'] = $status;
299       $row['#attributes']['class'][] = 'incompatible';
300     }
301
302     // If this module requires other modules, add them to the array.
303     foreach ($module->requires as $dependency => $version) {
304       if (!isset($modules[$dependency])) {
305         $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', ['@module' => Unicode::ucfirst($dependency)]);
306         $row['enable']['#disabled'] = TRUE;
307       }
308       // Only display visible modules.
309       elseif (empty($modules[$dependency]->hidden)) {
310         $name = $modules[$dependency]->info['name'];
311         // Disable the module's checkbox if it is incompatible with the
312         // dependency's version.
313         if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
314           $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> version @version)', [
315             '@module' => $name . $incompatible_version,
316             '@version' => $modules[$dependency]->info['version'],
317           ]);
318           $row['enable']['#disabled'] = TRUE;
319         }
320         // Disable the checkbox if the dependency is incompatible with this
321         // version of Drupal core.
322         elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
323           $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
324             '@module' => $name,
325           ]);
326           $row['enable']['#disabled'] = TRUE;
327         }
328         elseif ($modules[$dependency]->status) {
329           $row['#requires'][$dependency] = $this->t('@module', ['@module' => $name]);
330         }
331         else {
332           $row['#requires'][$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
333         }
334       }
335     }
336
337     // If this module is required by other modules, list those, and then make it
338     // impossible to disable this one.
339     foreach ($module->required_by as $dependent => $version) {
340       if (isset($modules[$dependent]) && empty($modules[$dependent]->info['hidden'])) {
341         if ($modules[$dependent]->status == 1 && $module->status == 1) {
342           $row['#required_by'][$dependent] = $this->t('@module', ['@module' => $modules[$dependent]->info['name']]);
343           $row['enable']['#disabled'] = TRUE;
344         }
345         else {
346           $row['#required_by'][$dependent] = $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $modules[$dependent]->info['name']]);
347         }
348       }
349     }
350
351     return $row;
352   }
353
354   /**
355    * Helper function for building a list of modules to install.
356    *
357    * @param \Drupal\Core\Form\FormStateInterface $form_state
358    *   The current state of the form.
359    *
360    * @return array
361    *   An array of modules to install and their dependencies.
362    */
363   protected function buildModuleList(FormStateInterface $form_state) {
364     // Build a list of modules to install.
365     $modules = [
366       'install' => [],
367       'dependencies' => [],
368       'experimental' => [],
369     ];
370
371     $data = system_rebuild_module_data();
372     foreach ($data as $name => $module) {
373       // If the module is installed there is nothing to do.
374       if ($this->moduleHandler->moduleExists($name)) {
375         continue;
376       }
377       // Required modules have to be installed.
378       if (!empty($module->required)) {
379         $modules['install'][$name] = $module->info['name'];
380       }
381       // Selected modules should be installed.
382       elseif (($checkbox = $form_state->getValue(['modules', $name], FALSE)) && $checkbox['enable']) {
383         $modules['install'][$name] = $data[$name]->info['name'];
384         // Identify experimental modules.
385         if ($data[$name]->info['package'] == 'Core (Experimental)') {
386           $modules['experimental'][$name] = $data[$name]->info['name'];
387         }
388       }
389     }
390
391     // Add all dependencies to a list.
392     foreach ($modules['install'] as $module => $value) {
393       foreach (array_keys($data[$module]->requires) as $dependency) {
394         if (!isset($modules['install'][$dependency]) && !$this->moduleHandler->moduleExists($dependency)) {
395           $modules['dependencies'][$module][$dependency] = $data[$dependency]->info['name'];
396           $modules['install'][$dependency] = $data[$dependency]->info['name'];
397
398           // Identify experimental modules.
399           if ($data[$dependency]->info['package'] == 'Core (Experimental)') {
400             $modules['experimental'][$dependency] = $data[$dependency]->info['name'];
401           }
402         }
403       }
404     }
405
406     // Make sure the install API is available.
407     include_once DRUPAL_ROOT . '/core/includes/install.inc';
408
409     // Invoke hook_requirements('install'). If failures are detected, make
410     // sure the dependent modules aren't installed either.
411     foreach (array_keys($modules['install']) as $module) {
412       if (!drupal_check_module($module)) {
413         unset($modules['install'][$module]);
414         unset($modules['experimental'][$module]);
415         foreach (array_keys($data[$module]->required_by) as $dependent) {
416           unset($modules['install'][$dependent]);
417           unset($modules['dependencies'][$dependent]);
418         }
419       }
420     }
421
422     return $modules;
423   }
424
425   /**
426    * {@inheritdoc}
427    */
428   public function submitForm(array &$form, FormStateInterface $form_state) {
429     // Retrieve a list of modules to install and their dependencies.
430     $modules = $this->buildModuleList($form_state);
431
432     // Redirect to a confirmation form if needed.
433     if (!empty($modules['experimental']) || !empty($modules['dependencies'])) {
434
435       $route_name = !empty($modules['experimental']) ? 'system.modules_list_experimental_confirm' : 'system.modules_list_confirm';
436       // Write the list of changed module states into a key value store.
437       $account = $this->currentUser()->id();
438       $this->keyValueExpirable->setWithExpire($account, $modules, 60);
439
440       // Redirect to the confirmation form.
441       $form_state->setRedirect($route_name);
442
443       // We can exit here because at least one modules has dependencies
444       // which we have to prompt the user for in a confirmation form.
445       return;
446     }
447
448     // Install the given modules.
449     if (!empty($modules['install'])) {
450       try {
451         $this->moduleInstaller->install(array_keys($modules['install']));
452         $module_names = array_values($modules['install']);
453         drupal_set_message($this->formatPlural(count($module_names), 'Module %name has been enabled.', '@count modules have been enabled: %names.', [
454           '%name' => $module_names[0],
455           '%names' => implode(', ', $module_names),
456         ]));
457       }
458       catch (PreExistingConfigException $e) {
459         $config_objects = $e->flattenConfigObjects($e->getConfigObjects());
460         drupal_set_message(
461           $this->formatPlural(
462             count($config_objects),
463             'Unable to install @extension, %config_names already exists in active configuration.',
464             'Unable to install @extension, %config_names already exist in active configuration.',
465             [
466               '%config_names' => implode(', ', $config_objects),
467               '@extension' => $modules['install'][$e->getExtension()]
468             ]),
469           'error'
470         );
471         return;
472       }
473       catch (UnmetDependenciesException $e) {
474         drupal_set_message(
475           $e->getTranslatedMessage($this->getStringTranslation(), $modules['install'][$e->getExtension()]),
476           'error'
477         );
478         return;
479       }
480     }
481   }
482
483 }