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