Security update for Core, with self-updated composer
[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         // 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       \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old'));
308       if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
309         // Rebuild routes after installing module. This is done here on top of
310         // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
311         // fastCGI which executes ::destruct() after the module installation
312         // page was sent already.
313         \Drupal::service('router.builder')->rebuild();
314       }
315
316       $this->moduleHandler->invokeAll('modules_installed', [$modules_installed]);
317     }
318
319     return TRUE;
320   }
321
322   /**
323    * {@inheritdoc}
324    */
325   public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
326     // Get all module data so we can find dependencies and sort.
327     $module_data = system_rebuild_module_data();
328     $module_list = $module_list ? array_combine($module_list, $module_list) : [];
329     if (array_diff_key($module_list, $module_data)) {
330       // One or more of the given modules doesn't exist.
331       return FALSE;
332     }
333
334     $extension_config = \Drupal::configFactory()->getEditable('core.extension');
335     $installed_modules = $extension_config->get('module') ?: [];
336     if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
337       // Nothing to do. All modules already uninstalled.
338       return TRUE;
339     }
340
341     if ($uninstall_dependents) {
342       // Add dependent modules to the list. The new modules will be processed as
343       // the while loop continues.
344       $profile = drupal_get_profile();
345       while (list($module) = each($module_list)) {
346         foreach (array_keys($module_data[$module]->required_by) as $dependent) {
347           if (!isset($module_data[$dependent])) {
348             // The dependent module does not exist.
349             return FALSE;
350           }
351
352           // Skip already uninstalled modules.
353           if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
354             $module_list[$dependent] = $dependent;
355           }
356         }
357       }
358     }
359
360     // Use the validators and throw an exception with the reasons.
361     if ($reasons = $this->validateUninstall($module_list)) {
362       foreach ($reasons as $reason) {
363         $reason_message[] = implode(', ', $reason);
364       }
365       throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message));
366     }
367     // Set the actual module weights.
368     $module_list = array_map(function ($module) use ($module_data) {
369       return $module_data[$module]->sort;
370     }, $module_list);
371
372     // Sort the module list by their weights.
373     asort($module_list);
374     $module_list = array_keys($module_list);
375
376     // Only process modules that are enabled. A module is only enabled if it is
377     // configured as enabled. Custom or overridden module handlers might contain
378     // the module already, which means that it might be loaded, but not
379     // necessarily installed.
380     foreach ($module_list as $module) {
381
382       // Clean up all entity bundles (including fields) of every entity type
383       // provided by the module that is being uninstalled.
384       // @todo Clean this up in https://www.drupal.org/node/2350111.
385       $entity_manager = \Drupal::entityManager();
386       foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
387         if ($entity_type->getProvider() == $module) {
388           foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
389             $entity_manager->onBundleDelete($bundle, $entity_type_id);
390           }
391         }
392       }
393
394       // Allow modules to react prior to the uninstallation of a module.
395       $this->moduleHandler->invokeAll('module_preuninstall', [$module]);
396
397       // Uninstall the module.
398       module_load_install($module);
399       $this->moduleHandler->invoke($module, 'uninstall');
400
401       // Remove all configuration belonging to the module.
402       \Drupal::service('config.manager')->uninstall('module', $module);
403
404       // In order to make uninstalling transactional if anything uses routes.
405       \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
406       \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
407
408       // Notify interested components that this module's entity types are being
409       // deleted. For example, a SQL-based storage handler can use this as an
410       // opportunity to drop the corresponding database tables.
411       // @todo Clean this up in https://www.drupal.org/node/2350111.
412       $update_manager = \Drupal::entityDefinitionUpdateManager();
413       foreach ($entity_manager->getDefinitions() as $entity_type) {
414         if ($entity_type->getProvider() == $module) {
415           $update_manager->uninstallEntityType($entity_type);
416         }
417         elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
418           // The module being installed may be adding new fields to existing
419           // entity types. Field definitions for any entity type defined by
420           // the module are handled in the if branch.
421           $entity_type_id = $entity_type->id();
422           /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
423           $storage = $entity_manager->getStorage($entity_type_id);
424           foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
425             // @todo We need to trigger field purging here.
426             //   See https://www.drupal.org/node/2282119.
427             if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
428               $update_manager->uninstallFieldStorageDefinition($storage_definition);
429             }
430           }
431         }
432       }
433
434       // Remove the schema.
435       drupal_uninstall_schema($module);
436
437       // Remove the module's entry from the config. Don't check schema when
438       // uninstalling a module since we are only clearing a key.
439       \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
440
441       // Update the module handler to remove the module.
442       // The current ModuleHandler instance is obsolete with the kernel rebuild
443       // below.
444       $module_filenames = $this->moduleHandler->getModuleList();
445       unset($module_filenames[$module]);
446       $this->moduleHandler->setModuleList($module_filenames);
447
448       // Remove any potential cache bins provided by the module.
449       $this->removeCacheBins($module);
450
451       // Clear the static cache of system_rebuild_module_data() to pick up the
452       // new module, since it merges the installation status of modules into
453       // its statically cached list.
454       drupal_static_reset('system_rebuild_module_data');
455
456       // Clear plugin manager caches.
457       \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
458
459       // Update the kernel to exclude the uninstalled modules.
460       $this->updateKernel($module_filenames);
461
462       // Update the theme registry to remove the newly uninstalled module.
463       drupal_theme_rebuild();
464
465       // Modules can alter theme info, so refresh theme data.
466       // @todo ThemeHandler cannot be injected into ModuleHandler, since that
467       //   causes a circular service dependency.
468       // @see https://www.drupal.org/node/2208429
469       \Drupal::service('theme_handler')->refreshInfo();
470
471       \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
472
473       $schema_store = \Drupal::keyValue('system.schema');
474       $schema_store->delete($module);
475
476       /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
477       $post_update_registry = \Drupal::service('update.post_update_registry');
478       $post_update_registry->filterOutInvokedUpdatesByModule($module);
479     }
480     // Rebuild routes after installing module. This is done here on top of
481     // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
482     // fastCGI which executes ::destruct() after the Module uninstallation page
483     // was sent already.
484     \Drupal::service('router.builder')->rebuild();
485     drupal_get_installed_schema_version(NULL, TRUE);
486
487     // Let other modules react.
488     $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list]);
489
490     // Flush all persistent caches.
491     // Any cache entry might implicitly depend on the uninstalled modules,
492     // so clear all of them explicitly.
493     $this->moduleHandler->invokeAll('cache_flush');
494     foreach (Cache::getBins() as $service_id => $cache_backend) {
495       $cache_backend->deleteAll();
496     }
497
498     return TRUE;
499   }
500
501   /**
502    * Helper method for removing all cache bins registered by a given module.
503    *
504    * @param string $module
505    *   The name of the module for which to remove all registered cache bins.
506    */
507   protected function removeCacheBins($module) {
508     $service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
509     if (!file_exists($service_yaml_file)) {
510       return;
511     }
512
513     $definitions = Yaml::decode(file_get_contents($service_yaml_file));
514
515     $cache_bin_services = array_filter(
516       isset($definitions['services']) ? $definitions['services'] : [],
517       function ($definition) {
518         $tags = isset($definition['tags']) ? $definition['tags'] : [];
519         foreach ($tags as $tag) {
520           if (isset($tag['name']) && ($tag['name'] == 'cache.bin')) {
521             return TRUE;
522           }
523         }
524         return FALSE;
525       }
526     );
527
528     foreach (array_keys($cache_bin_services) as $service_id) {
529       $backend = $this->kernel->getContainer()->get($service_id);
530       if ($backend instanceof CacheBackendInterface) {
531         $backend->removeBin();
532       }
533     }
534   }
535
536   /**
537    * Updates the kernel module list.
538    *
539    * @param string $module_filenames
540    *   The list of installed modules.
541    */
542   protected function updateKernel($module_filenames) {
543     // This reboots the kernel to register the module's bundle and its services
544     // in the service container. The $module_filenames argument is taken over as
545     // %container.modules% parameter, which is passed to a fresh ModuleHandler
546     // instance upon first retrieval.
547     $this->kernel->updateModules($module_filenames, $module_filenames);
548     // After rebuilding the container we need to update the injected
549     // dependencies.
550     $container = $this->kernel->getContainer();
551     $this->moduleHandler = $container->get('module_handler');
552   }
553
554   /**
555    * {@inheritdoc}
556    */
557   public function validateUninstall(array $module_list) {
558     $reasons = [];
559     foreach ($module_list as $module) {
560       foreach ($this->uninstallValidators as $validator) {
561         $validation_reasons = $validator->validate($module);
562         if (!empty($validation_reasons)) {
563           if (!isset($reasons[$module])) {
564             $reasons[$module] = [];
565           }
566           $reasons[$module] = array_merge($reasons[$module], $validation_reasons);
567         }
568       }
569     }
570     return $reasons;
571   }
572
573 }