Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Extension / ModuleInstaller.php
1 <?php
2
3 namespace Drupal\Core\Extension;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheBackendInterface;
7 use Drupal\Core\DrupalKernelInterface;
8 use Drupal\Core\Entity\EntityStorageException;
9 use Drupal\Core\Entity\FieldableEntityInterface;
10 use Drupal\Core\Serialization\Yaml;
11
12 /**
13  * Default implementation of the module installer.
14  *
15  * It registers the module in config, installs its own configuration,
16  * installs the schema, updates the Drupal kernel and more.
17  *
18  * We don't inject dependencies yet, as we would need to reload them after
19  * each installation or uninstallation of a module.
20  * https://www.drupal.org/project/drupal/issues/2350111 for example tries to
21  * solve this dilemma.
22  */
23 class ModuleInstaller implements ModuleInstallerInterface {
24
25   /**
26    * The module handler.
27    *
28    * @var \Drupal\Core\Extension\ModuleHandlerInterface
29    */
30   protected $moduleHandler;
31
32   /**
33    * The drupal kernel.
34    *
35    * @var \Drupal\Core\DrupalKernelInterface
36    */
37   protected $kernel;
38
39   /**
40    * The app root.
41    *
42    * @var string
43    */
44   protected $root;
45
46   /**
47    * The uninstall validators.
48    *
49    * @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[]
50    */
51   protected $uninstallValidators;
52
53   /**
54    * Constructs a new ModuleInstaller instance.
55    *
56    * @param string $root
57    *   The app root.
58    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
59    *   The module handler.
60    * @param \Drupal\Core\DrupalKernelInterface $kernel
61    *   The drupal kernel.
62    *
63    * @see \Drupal\Core\DrupalKernel
64    * @see \Drupal\Core\CoreServiceProvider
65    */
66   public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
67     $this->root = $root;
68     $this->moduleHandler = $module_handler;
69     $this->kernel = $kernel;
70   }
71
72   /**
73    * {@inheritdoc}
74    */
75   public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) {
76     $this->uninstallValidators[] = $uninstall_validator;
77   }
78
79   /**
80    * {@inheritdoc}
81    */
82   public function install(array $module_list, $enable_dependencies = TRUE) {
83     $extension_config = \Drupal::configFactory()->getEditable('core.extension');
84     if ($enable_dependencies) {
85       // Get all module data so we can find dependencies and sort.
86       $module_data = system_rebuild_module_data();
87       $module_list = $module_list ? array_combine($module_list, $module_list) : [];
88       if ($missing_modules = array_diff_key($module_list, $module_data)) {
89         // One or more of the given modules doesn't exist.
90         throw new MissingDependencyException(sprintf('Unable to install modules %s due to missing modules %s.', implode(', ', $module_list), implode(', ', $missing_modules)));
91       }
92
93       // Only process currently uninstalled modules.
94       $installed_modules = $extension_config->get('module') ?: [];
95       if (!$module_list = array_diff_key($module_list, $installed_modules)) {
96         // Nothing to do. All modules already installed.
97         return TRUE;
98       }
99
100       // Add dependencies to the list. The new modules will be processed as
101       // the foreach loop continues.
102       foreach ($module_list as $module => $value) {
103         foreach (array_keys($module_data[$module]->requires) as $dependency) {
104           if (!isset($module_data[$dependency])) {
105             // The dependency does not exist.
106             throw new MissingDependencyException("Unable to install modules: module '$module' is missing its dependency module $dependency.");
107           }
108
109           // Skip already installed modules.
110           if (!isset($module_list[$dependency]) && !isset($installed_modules[$dependency])) {
111             $module_list[$dependency] = $dependency;
112           }
113         }
114       }
115
116       // Set the actual module weights.
117       $module_list = array_map(function ($module) use ($module_data) {
118         return $module_data[$module]->sort;
119       }, $module_list);
120
121       // Sort the module list by their weights (reverse).
122       arsort($module_list);
123       $module_list = array_keys($module_list);
124     }
125
126     // Required for module installation checks.
127     include_once $this->root . '/core/includes/install.inc';
128
129     /** @var \Drupal\Core\Config\ConfigInstaller $config_installer */
130     $config_installer = \Drupal::service('config.installer');
131     $sync_status = $config_installer->isSyncing();
132     if ($sync_status) {
133       $source_storage = $config_installer->getSourceStorage();
134     }
135     $modules_installed = [];
136     foreach ($module_list as $module) {
137       $enabled = $extension_config->get("module.$module") !== NULL;
138       if (!$enabled) {
139         // Throw an exception if the module name is too long.
140         if (strlen($module) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
141           throw new ExtensionNameLengthException("Module name '$module' is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters');
142         }
143
144         // Load a new config object for each iteration, otherwise changes made
145         // in hook_install() are not reflected in $extension_config.
146         $extension_config = \Drupal::configFactory()->getEditable('core.extension');
147
148         // Check the validity of the default configuration. This will throw
149         // exceptions if the configuration is not valid.
150         $config_installer->checkConfigurationToInstall('module', $module);
151
152         // Save this data without checking schema. This is a performance
153         // improvement for module installation.
154         $extension_config
155           ->set("module.$module", 0)
156           ->set('module', module_config_sort($extension_config->get('module')))
157           ->save(TRUE);
158
159         // Prepare the new module list, sorted by weight, including filenames.
160         // This list is used for both the ModuleHandler and DrupalKernel. It
161         // needs to be kept in sync between both. A DrupalKernel reboot or
162         // rebuild will automatically re-instantiate a new ModuleHandler that
163         // uses the new module list of the kernel. However, DrupalKernel does
164         // not cause any modules to be loaded.
165         // Furthermore, the currently active (fixed) module list can be
166         // different from the configured list of enabled modules. For all active
167         // modules not contained in the configured enabled modules, we assume a
168         // weight of 0.
169         $current_module_filenames = $this->moduleHandler->getModuleList();
170         $current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
171         $current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
172         $module_filenames = [];
173         foreach ($current_modules as $name => $weight) {
174           if (isset($current_module_filenames[$name])) {
175             $module_filenames[$name] = $current_module_filenames[$name];
176           }
177           else {
178             $module_path = \Drupal::service('extension.list.module')->getPath($name);
179             $pathname = "$module_path/$name.info.yml";
180             $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
181             $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename);
182           }
183         }
184
185         // Update the module handler in order to load the module's code.
186         // This allows the module to participate in hooks and its existence to
187         // be discovered by other modules.
188         // The current ModuleHandler instance is obsolete with the kernel
189         // rebuild below.
190         $this->moduleHandler->setModuleList($module_filenames);
191         $this->moduleHandler->load($module);
192         module_load_install($module);
193
194         // Clear the static cache of the "extension.list.module" service to pick
195         // up the new module, since it merges the installation status of modules
196         // into its statically cached list.
197         \Drupal::service('extension.list.module')->reset();
198
199         // Update the kernel to include it.
200         $this->updateKernel($module_filenames);
201
202         // Replace the route provider service with a version that will rebuild
203         // if routes used during installation. This ensures that a module's
204         // routes are available during installation. This has to occur before
205         // any services that depend on it are instantiated otherwise those
206         // services will have the old route provider injected. Note that, since
207         // the container is rebuilt by updating the kernel, the route provider
208         // service is the regular one even though we are in a loop and might
209         // have replaced it before.
210         \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
211         \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
212
213         // Allow modules to react prior to the installation of a module.
214         $this->moduleHandler->invokeAll('module_preinstall', [$module]);
215
216         // Now install the module's schema if necessary.
217         drupal_install_schema($module);
218
219         // Clear plugin manager caches.
220         \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
221
222         // Set the schema version to the number of the last update provided by
223         // the module, or the minimum core schema version.
224         $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
225         $versions = drupal_get_schema_versions($module);
226         if ($versions) {
227           $version = max(max($versions), $version);
228         }
229
230         // Notify interested components that this module's entity types and
231         // field storage definitions are new. For example, a SQL-based storage
232         // handler can use this as an opportunity to create the necessary
233         // database tables.
234         // @todo Clean this up in https://www.drupal.org/node/2350111.
235         $entity_manager = \Drupal::entityManager();
236         $update_manager = \Drupal::entityDefinitionUpdateManager();
237         foreach ($entity_manager->getDefinitions() as $entity_type) {
238           if ($entity_type->getProvider() == $module) {
239             $update_manager->installEntityType($entity_type);
240           }
241           elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
242             // The module being installed may be adding new fields to existing
243             // entity types. Field definitions for any entity type defined by
244             // the module are handled in the if branch.
245             foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
246               if ($storage_definition->getProvider() == $module) {
247                 // If the module being installed is also defining a storage key
248                 // for the entity type, the entity schema may not exist yet. It
249                 // will be created later in that case.
250                 try {
251                   $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
252                 }
253                 catch (EntityStorageException $e) {
254                   watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
255                 }
256               }
257             }
258           }
259         }
260
261         // Install default configuration of the module.
262         $config_installer = \Drupal::service('config.installer');
263         if ($sync_status) {
264           $config_installer
265             ->setSyncing(TRUE)
266             ->setSourceStorage($source_storage);
267         }
268         \Drupal::service('config.installer')->installDefaultConfig('module', $module);
269
270         // If the module has no current updates, but has some that were
271         // previously removed, set the version to the value of
272         // hook_update_last_removed().
273         if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) {
274           $version = max($version, $last_removed);
275         }
276         drupal_set_installed_schema_version($module, $version);
277
278         // Ensure that all post_update functions are registered already.
279         /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
280         $post_update_registry = \Drupal::service('update.post_update_registry');
281         $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
282
283         // Record the fact that it was installed.
284         $modules_installed[] = $module;
285
286         // Drupal's stream wrappers needs to be re-registered in case a
287         // module-provided stream wrapper is used later in the same request. In
288         // particular, this happens when installing Drupal via Drush, as the
289         // 'translations' stream wrapper is provided by Interface Translation
290         // module and is later used to import translations.
291         \Drupal::service('stream_wrapper_manager')->register();
292
293         // Update the theme registry to include it.
294         drupal_theme_rebuild();
295
296         // Modules can alter theme info, so refresh theme data.
297         // @todo ThemeHandler cannot be injected into ModuleHandler, since that
298         //   causes a circular service dependency.
299         // @see https://www.drupal.org/node/2208429
300         \Drupal::service('theme_handler')->refreshInfo();
301
302         // Allow the module to perform install tasks.
303         $this->moduleHandler->invoke($module, 'install');
304
305         // Record the fact that it was installed.
306         \Drupal::logger('system')->info('%module module installed.', ['%module' => $module]);
307       }
308     }
309
310     // If any modules were newly installed, invoke hook_modules_installed().
311     if (!empty($modules_installed)) {
312       // If the container was rebuilt during hook_install() it might not have
313       // the 'router.route_provider.old' service.
314       if (\Drupal::hasService('router.route_provider.old')) {
315         \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old'));
316       }
317       if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
318         // Rebuild routes after installing module. This is done here on top of
319         // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
320         // fastCGI which executes ::destruct() after the module installation
321         // page was sent already.
322         \Drupal::service('router.builder')->rebuild();
323       }
324
325       $this->moduleHandler->invokeAll('modules_installed', [$modules_installed]);
326     }
327
328     return TRUE;
329   }
330
331   /**
332    * {@inheritdoc}
333    */
334   public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
335     // Get all module data so we can find dependencies and sort.
336     $module_data = system_rebuild_module_data();
337     $module_list = $module_list ? array_combine($module_list, $module_list) : [];
338     if (array_diff_key($module_list, $module_data)) {
339       // One or more of the given modules doesn't exist.
340       return FALSE;
341     }
342
343     $extension_config = \Drupal::configFactory()->getEditable('core.extension');
344     $installed_modules = $extension_config->get('module') ?: [];
345     if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
346       // Nothing to do. All modules already uninstalled.
347       return TRUE;
348     }
349
350     if ($uninstall_dependents) {
351       // Add dependent modules to the list. The new modules will be processed as
352       // the foreach loop continues.
353       foreach ($module_list as $module => $value) {
354         foreach (array_keys($module_data[$module]->required_by) as $dependent) {
355           if (!isset($module_data[$dependent])) {
356             // The dependent module does not exist.
357             return FALSE;
358           }
359
360           // Skip already uninstalled modules.
361           if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent])) {
362             $module_list[$dependent] = $dependent;
363           }
364         }
365       }
366     }
367
368     // Use the validators and throw an exception with the reasons.
369     if ($reasons = $this->validateUninstall($module_list)) {
370       foreach ($reasons as $reason) {
371         $reason_message[] = implode(', ', $reason);
372       }
373       throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message));
374     }
375     // Set the actual module weights.
376     $module_list = array_map(function ($module) use ($module_data) {
377       return $module_data[$module]->sort;
378     }, $module_list);
379
380     // Sort the module list by their weights.
381     asort($module_list);
382     $module_list = array_keys($module_list);
383
384     // Only process modules that are enabled. A module is only enabled if it is
385     // configured as enabled. Custom or overridden module handlers might contain
386     // the module already, which means that it might be loaded, but not
387     // necessarily installed.
388     foreach ($module_list as $module) {
389
390       // Clean up all entity bundles (including fields) of every entity type
391       // provided by the module that is being uninstalled.
392       // @todo Clean this up in https://www.drupal.org/node/2350111.
393       $entity_manager = \Drupal::entityManager();
394       foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
395         if ($entity_type->getProvider() == $module) {
396           foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
397             $entity_manager->onBundleDelete($bundle, $entity_type_id);
398           }
399         }
400       }
401
402       // Allow modules to react prior to the uninstallation of a module.
403       $this->moduleHandler->invokeAll('module_preuninstall', [$module]);
404
405       // Uninstall the module.
406       module_load_install($module);
407       $this->moduleHandler->invoke($module, 'uninstall');
408
409       // Remove all configuration belonging to the module.
410       \Drupal::service('config.manager')->uninstall('module', $module);
411
412       // In order to make uninstalling transactional if anything uses routes.
413       \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
414       \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
415
416       // Notify interested components that this module's entity types are being
417       // deleted. For example, a SQL-based storage handler can use this as an
418       // opportunity to drop the corresponding database tables.
419       // @todo Clean this up in https://www.drupal.org/node/2350111.
420       $update_manager = \Drupal::entityDefinitionUpdateManager();
421       foreach ($entity_manager->getDefinitions() as $entity_type) {
422         if ($entity_type->getProvider() == $module) {
423           $update_manager->uninstallEntityType($entity_type);
424         }
425         elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
426           // The module being uninstalled might have added new fields to
427           // existing entity types. This will add them to the deleted fields
428           // repository so their data will be purged on cron.
429           foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
430             if ($storage_definition->getProvider() == $module) {
431               $update_manager->uninstallFieldStorageDefinition($storage_definition);
432             }
433           }
434         }
435       }
436
437       // Remove the schema.
438       drupal_uninstall_schema($module);
439
440       // Remove the module's entry from the config. Don't check schema when
441       // uninstalling a module since we are only clearing a key.
442       \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
443
444       // Update the module handler to remove the module.
445       // The current ModuleHandler instance is obsolete with the kernel rebuild
446       // below.
447       $module_filenames = $this->moduleHandler->getModuleList();
448       unset($module_filenames[$module]);
449       $this->moduleHandler->setModuleList($module_filenames);
450
451       // Remove any potential cache bins provided by the module.
452       $this->removeCacheBins($module);
453
454       // Clear the static cache of the "extension.list.module" service to pick
455       // up the new module, since it merges the installation status of modules
456       // into its statically cached list.
457       \Drupal::service('extension.list.module')->reset();
458
459       // Clear plugin manager caches.
460       \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
461
462       // Update the kernel to exclude the uninstalled modules.
463       $this->updateKernel($module_filenames);
464
465       // Update the theme registry to remove the newly uninstalled module.
466       drupal_theme_rebuild();
467
468       // Modules can alter theme info, so refresh theme data.
469       // @todo ThemeHandler cannot be injected into ModuleHandler, since that
470       //   causes a circular service dependency.
471       // @see https://www.drupal.org/node/2208429
472       \Drupal::service('theme_handler')->refreshInfo();
473
474       \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
475
476       $schema_store = \Drupal::keyValue('system.schema');
477       $schema_store->delete($module);
478
479       /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
480       $post_update_registry = \Drupal::service('update.post_update_registry');
481       $post_update_registry->filterOutInvokedUpdatesByModule($module);
482     }
483     // Rebuild routes after installing module. This is done here on top of
484     // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
485     // fastCGI which executes ::destruct() after the Module uninstallation page
486     // was sent already.
487     \Drupal::service('router.builder')->rebuild();
488     drupal_get_installed_schema_version(NULL, TRUE);
489
490     // Let other modules react.
491     $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list]);
492
493     // Flush all persistent caches.
494     // Any cache entry might implicitly depend on the uninstalled modules,
495     // so clear all of them explicitly.
496     $this->moduleHandler->invokeAll('cache_flush');
497     foreach (Cache::getBins() as $service_id => $cache_backend) {
498       $cache_backend->deleteAll();
499     }
500
501     return TRUE;
502   }
503
504   /**
505    * Helper method for removing all cache bins registered by a given module.
506    *
507    * @param string $module
508    *   The name of the module for which to remove all registered cache bins.
509    */
510   protected function removeCacheBins($module) {
511     $service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
512     if (!file_exists($service_yaml_file)) {
513       return;
514     }
515
516     $definitions = Yaml::decode(file_get_contents($service_yaml_file));
517
518     $cache_bin_services = array_filter(
519       isset($definitions['services']) ? $definitions['services'] : [],
520       function ($definition) {
521         $tags = isset($definition['tags']) ? $definition['tags'] : [];
522         foreach ($tags as $tag) {
523           if (isset($tag['name']) && ($tag['name'] == 'cache.bin')) {
524             return TRUE;
525           }
526         }
527         return FALSE;
528       }
529     );
530
531     foreach (array_keys($cache_bin_services) as $service_id) {
532       $backend = $this->kernel->getContainer()->get($service_id);
533       if ($backend instanceof CacheBackendInterface) {
534         $backend->removeBin();
535       }
536     }
537   }
538
539   /**
540    * Updates the kernel module list.
541    *
542    * @param string $module_filenames
543    *   The list of installed modules.
544    */
545   protected function updateKernel($module_filenames) {
546     // This reboots the kernel to register the module's bundle and its services
547     // in the service container. The $module_filenames argument is taken over as
548     // %container.modules% parameter, which is passed to a fresh ModuleHandler
549     // instance upon first retrieval.
550     $this->kernel->updateModules($module_filenames, $module_filenames);
551     // After rebuilding the container we need to update the injected
552     // dependencies.
553     $container = $this->kernel->getContainer();
554     $this->moduleHandler = $container->get('module_handler');
555   }
556
557   /**
558    * {@inheritdoc}
559    */
560   public function validateUninstall(array $module_list) {
561     $reasons = [];
562     foreach ($module_list as $module) {
563       foreach ($this->uninstallValidators as $validator) {
564         $validation_reasons = $validator->validate($module);
565         if (!empty($validation_reasons)) {
566           if (!isset($reasons[$module])) {
567             $reasons[$module] = [];
568           }
569           $reasons[$module] = array_merge($reasons[$module], $validation_reasons);
570         }
571       }
572     }
573     return $reasons;
574   }
575
576 }