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