Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / EventSubscriber / ConfigImportSubscriber.php
1 <?php
2
3 namespace Drupal\Core\EventSubscriber;
4
5 use Drupal\Core\Config\Config;
6 use Drupal\Core\Config\ConfigImporter;
7 use Drupal\Core\Config\ConfigImporterEvent;
8 use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
9 use Drupal\Core\Config\ConfigNameException;
10 use Drupal\Core\Extension\ThemeHandlerInterface;
11
12 /**
13  * Config import subscriber for config import events.
14  */
15 class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
16
17   /**
18    * Theme data.
19    *
20    * @var \Drupal\Core\Extension\Extension[]
21    */
22   protected $themeData;
23
24   /**
25    * Module data.
26    *
27    * @var \Drupal\Core\Extension\Extension[]
28    */
29   protected $moduleData;
30
31   /**
32    * The theme handler.
33    *
34    * @var \Drupal\Core\Extension\ThemeHandlerInterface
35    */
36   protected $themeHandler;
37
38   /**
39    * Constructs the ConfigImportSubscriber.
40    *
41    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
42    *   The theme handler.
43    */
44   public function __construct(ThemeHandlerInterface $theme_handler) {
45     $this->themeHandler = $theme_handler;
46   }
47
48   /**
49    * Validates the configuration to be imported.
50    *
51    * @param \Drupal\Core\Config\ConfigImporterEvent $event
52    *   The Event to process.
53    *
54    * @throws \Drupal\Core\Config\ConfigNameException
55    */
56   public function onConfigImporterValidate(ConfigImporterEvent $event) {
57     foreach (['delete', 'create', 'update'] as $op) {
58       foreach ($event->getConfigImporter()->getUnprocessedConfiguration($op) as $name) {
59         try {
60           Config::validateName($name);
61         }
62         catch (ConfigNameException $e) {
63           $message = $this->t('The config name @config_name is invalid.', ['@config_name' => $name]);
64           $event->getConfigImporter()->logError($message);
65         }
66       }
67     }
68     $config_importer = $event->getConfigImporter();
69     if ($config_importer->getStorageComparer()->getSourceStorage()->exists('core.extension')) {
70       $this->validateModules($config_importer);
71       $this->validateThemes($config_importer);
72       $this->validateDependencies($config_importer);
73     }
74     else {
75       $config_importer->logError($this->t('The core.extension configuration does not exist.'));
76     }
77   }
78
79   /**
80    * Validates module installations and uninstallations.
81    *
82    * @param \Drupal\Core\Config\ConfigImporter $config_importer
83    *   The configuration importer.
84    */
85   protected function validateModules(ConfigImporter $config_importer) {
86     $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
87     // Get a list of modules with dependency weights as values.
88     $module_data = $this->getModuleData();
89     $nonexistent_modules = array_keys(array_diff_key($core_extension['module'], $module_data));
90     foreach ($nonexistent_modules as $module) {
91       $config_importer->logError($this->t('Unable to install the %module module since it does not exist.', ['%module' => $module]));
92     }
93
94     // Ensure that all modules being installed have their dependencies met.
95     $installs = $config_importer->getExtensionChangelist('module', 'install');
96     foreach ($installs as $module) {
97       $missing_dependencies = [];
98       foreach (array_keys($module_data[$module]->requires) as $required_module) {
99         if (!isset($core_extension['module'][$required_module])) {
100           $missing_dependencies[] = $module_data[$required_module]->info['name'];
101         }
102       }
103       if (!empty($missing_dependencies)) {
104         $module_name = $module_data[$module]->info['name'];
105         $message = $this->formatPlural(count($missing_dependencies),
106           'Unable to install the %module module since it requires the %required_module module.',
107           'Unable to install the %module module since it requires the %required_module modules.',
108           ['%module' => $module_name, '%required_module' => implode(', ', $missing_dependencies)]
109         );
110         $config_importer->logError($message);
111       }
112     }
113
114     // Get the install profile from the site's configuration.
115     $current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension');
116     $install_profile = isset($current_core_extension['profile']) ? $current_core_extension['profile'] : NULL;
117
118     // Ensure that all modules being uninstalled are not required by modules
119     // that will be installed after the import.
120     $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
121     foreach ($uninstalls as $module) {
122       foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
123         if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) {
124           $module_name = $module_data[$module]->info['name'];
125           $dependent_module_name = $module_data[$dependent_module]->info['name'];
126           $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', ['%module' => $module_name, '%dependent_module' => $dependent_module_name]));
127         }
128       }
129     }
130
131     // Ensure that the install profile is not being uninstalled.
132     if (in_array($install_profile, $uninstalls, TRUE)) {
133       $profile_name = $module_data[$install_profile]->info['name'];
134       $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the install profile.', ['%profile' => $profile_name]));
135     }
136
137     // Ensure the profile is not changing.
138     if ($install_profile !== $core_extension['profile']) {
139       $config_importer->logError($this->t('Cannot change the install profile from %profile to %new_profile once Drupal is installed.', ['%profile' => $install_profile, '%new_profile' => $core_extension['profile']]));
140     }
141   }
142
143   /**
144    * Validates theme installations and uninstallations.
145    *
146    * @param \Drupal\Core\Config\ConfigImporter $config_importer
147    *   The configuration importer.
148    */
149   protected function validateThemes(ConfigImporter $config_importer) {
150     $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
151     // Get all themes including those that are not installed.
152     $theme_data = $this->getThemeData();
153     $installs = $config_importer->getExtensionChangelist('theme', 'install');
154     foreach ($installs as $key => $theme) {
155       if (!isset($theme_data[$theme])) {
156         $config_importer->logError($this->t('Unable to install the %theme theme since it does not exist.', ['%theme' => $theme]));
157         // Remove non-existing installs from the list so we can validate theme
158         // dependencies later.
159         unset($installs[$key]);
160       }
161     }
162
163     // Ensure that all themes being installed have their dependencies met.
164     foreach ($installs as $theme) {
165       foreach (array_keys($theme_data[$theme]->requires) as $required_theme) {
166         if (!isset($core_extension['theme'][$required_theme])) {
167           $theme_name = $theme_data[$theme]->info['name'];
168           $required_theme_name = $theme_data[$required_theme]->info['name'];
169           $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_theme theme.', ['%theme' => $theme_name, '%required_theme' => $required_theme_name]));
170         }
171       }
172     }
173
174     // Ensure that all themes being uninstalled are not required by themes that
175     // will be installed after the import.
176     $uninstalls = $config_importer->getExtensionChangelist('theme', 'uninstall');
177     foreach ($uninstalls as $theme) {
178       foreach (array_keys($theme_data[$theme]->required_by) as $dependent_theme) {
179         if ($theme_data[$dependent_theme]->status && !in_array($dependent_theme, $uninstalls, TRUE)) {
180           $theme_name = $theme_data[$theme]->info['name'];
181           $dependent_theme_name = $theme_data[$dependent_theme]->info['name'];
182           $config_importer->logError($this->t('Unable to uninstall the %theme theme since the %dependent_theme theme is installed.', ['%theme' => $theme_name, '%dependent_theme' => $dependent_theme_name]));
183         }
184       }
185     }
186   }
187
188   /**
189    * Validates configuration being imported does not have unmet dependencies.
190    *
191    * @param \Drupal\Core\Config\ConfigImporter $config_importer
192    *   The configuration importer.
193    */
194   protected function validateDependencies(ConfigImporter $config_importer) {
195     $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
196     $existing_dependencies = [
197       'config' => $config_importer->getStorageComparer()->getSourceStorage()->listAll(),
198       'module' => array_keys($core_extension['module']),
199       'theme' => array_keys($core_extension['theme']),
200     ];
201
202     $theme_data = $this->getThemeData();
203     $module_data = $this->getModuleData();
204
205     // Validate the dependencies of all the configuration. We have to validate
206     // the entire tree because existing configuration might depend on
207     // configuration that is being deleted.
208     foreach ($config_importer->getStorageComparer()->getSourceStorage()->listAll() as $name) {
209       // Ensure that the config owner is installed. This checks all
210       // configuration including configuration entities.
211       list($owner,) = explode('.', $name, 2);
212       if ($owner !== 'core') {
213         $message = FALSE;
214         if (!isset($core_extension['module'][$owner]) && isset($module_data[$owner])) {
215           $message = $this->t('Configuration %name depends on the %owner module that will not be installed after import.', [
216             '%name' => $name,
217             '%owner' => $module_data[$owner]->info['name']
218           ]);
219         }
220         elseif (!isset($core_extension['theme'][$owner]) && isset($theme_data[$owner])) {
221           $message = $this->t('Configuration %name depends on the %owner theme that will not be installed after import.', [
222             '%name' => $name,
223             '%owner' => $theme_data[$owner]->info['name']
224           ]);
225         }
226         elseif (!isset($core_extension['module'][$owner]) && !isset($core_extension['theme'][$owner])) {
227           $message = $this->t('Configuration %name depends on the %owner extension that will not be installed after import.', [
228             '%name' => $name,
229             '%owner' => $owner
230           ]);
231         }
232
233         if ($message) {
234           $config_importer->logError($message);
235           continue;
236         }
237       }
238
239       $data = $config_importer->getStorageComparer()->getSourceStorage()->read($name);
240       // Configuration entities have dependencies on modules, themes, and other
241       // configuration entities that we can validate. Their content dependencies
242       // are not validated since we assume that they are soft dependencies.
243       // Configuration entities can be identified by having 'dependencies' and
244       // 'uuid' keys.
245       if (isset($data['dependencies']) && isset($data['uuid'])) {
246         $dependencies_to_check = array_intersect_key($data['dependencies'], array_flip(['module', 'theme', 'config']));
247         foreach ($dependencies_to_check as $type => $dependencies) {
248           $diffs = array_diff($dependencies, $existing_dependencies[$type]);
249           if (!empty($diffs)) {
250             $message = FALSE;
251             switch ($type) {
252               case 'module':
253                 $message = $this->formatPlural(
254                   count($diffs),
255                   'Configuration %name depends on the %module module that will not be installed after import.',
256                   'Configuration %name depends on modules (%module) that will not be installed after import.',
257                   ['%name' => $name, '%module' => implode(', ', $this->getNames($diffs, $module_data))]
258                 );
259                 break;
260               case 'theme':
261                 $message = $this->formatPlural(
262                   count($diffs),
263                   'Configuration %name depends on the %theme theme that will not be installed after import.',
264                   'Configuration %name depends on themes (%theme) that will not be installed after import.',
265                   ['%name' => $name, '%theme' => implode(', ', $this->getNames($diffs, $theme_data))]
266                 );
267                 break;
268               case 'config':
269                 $message = $this->formatPlural(
270                   count($diffs),
271                   'Configuration %name depends on the %config configuration that will not exist after import.',
272                   'Configuration %name depends on configuration (%config) that will not exist after import.',
273                   ['%name' => $name, '%config' => implode(', ', $diffs)]
274                 );
275                 break;
276             }
277
278             if ($message) {
279               $config_importer->logError($message);
280             }
281           }
282         }
283       }
284     }
285   }
286
287   /**
288    * Gets theme data.
289    *
290    * @return \Drupal\Core\Extension\Extension[]
291    */
292   protected function getThemeData() {
293     if (!isset($this->themeData)) {
294       $this->themeData = $this->themeHandler->rebuildThemeData();
295     }
296     return $this->themeData;
297   }
298
299   /**
300    * Gets module data.
301    *
302    * @return \Drupal\Core\Extension\Extension[]
303    */
304   protected function getModuleData() {
305     if (!isset($this->moduleData)) {
306       $this->moduleData = system_rebuild_module_data();
307     }
308     return $this->moduleData;
309   }
310
311   /**
312    * Gets human readable extension names.
313    *
314    * @param array $names
315    *   A list of extension machine names.
316    * @param \Drupal\Core\Extension\Extension[] $extension_data
317    *   Extension data.
318    *
319    * @return array
320    *   A list of human-readable extension names, or machine names if
321    *   human-readable names are not available.
322    */
323   protected function getNames(array $names, array $extension_data) {
324     return array_map(function ($name) use ($extension_data) {
325       if (isset($extension_data[$name])) {
326         $name = $extension_data[$name]->info['name'];
327       }
328       return $name;
329     }, $names);
330   }
331
332 }