ConfigImportSubscriber.php 14 KB

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