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