ConfigImportSubscriber.php 14 KB

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