protected function setUp() { parent::setUp(); $this->installEntitySchema('node'); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer($this->container->get('config.storage.staging'), $this->container->get('config.storage'), $this->container->get('config.manager')); $this->configImporter = new ConfigImporter($storage_comparer->createChangelist(), $this->container->get('event_dispatcher'), $this->container->get('config.manager'), $this->container->get('lock'), $this->container->get('config.typed'), $this->container->get('module_handler'), $this->container->get('theme_handler'), $this->container->get('string_translation')); }
protected function setUp() { parent::setUp(); $this->installConfig(array('config_test')); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. unset($GLOBALS['hook_config_test']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer($this->container->get('config.storage.staging'), $this->container->get('config.storage'), $this->container->get('config.manager')); $this->configImporter = new ConfigImporter($storage_comparer->createChangelist(), $this->container->get('event_dispatcher'), $this->container->get('config.manager'), $this->container->get('lock'), $this->container->get('config.typed'), $this->container->get('module_handler'), $this->container->get('module_installer'), $this->container->get('theme_handler'), $this->container->get('string_translation')); }
/** * Tests deleting a contact form entity via a configuration import. * * @see \Drupal\Core\Entity\Event\BundleConfigImportValidate */ public function testDeleteThroughImport() { $contact_form = ContactForm::create(['id' => 'test']); $contact_form->save(); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer($this->container->get('config.storage.sync'), $this->container->get('config.storage'), $this->container->get('config.manager')); $config_importer = new ConfigImporter($storage_comparer->createChangelist(), $this->container->get('event_dispatcher'), $this->container->get('config.manager'), $this->container->get('lock'), $this->container->get('config.typed'), $this->container->get('module_handler'), $this->container->get('module_installer'), $this->container->get('theme_handler'), $this->container->get('string_translation')); // Delete the contact message in sync. $sync = $this->container->get('config.storage.sync'); $sync->delete($contact_form->getConfigDependencyName()); // Import. $config_importer->reset()->import(); $this->assertNull(ContactForm::load($contact_form->id()), 'The contact form has been deleted.'); }
/** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $io = new DrupalStyle($input, $output); $directory = $input->getOption('directory'); if ($directory) { $configSyncDir = $directory; } else { $configSyncDir = config_get_config_directory(CONFIG_SYNC_DIRECTORY); } $source_storage = new FileStorage($configSyncDir); $storage_comparer = new StorageComparer($source_storage, $this->configStorage, $this->configManager); if (!$storage_comparer->createChangelist()->hasChanges()) { $io->success($this->trans('commands.config.import.messages.nothing-to-do')); } if ($this->configImport($io, $storage_comparer)) { $io->success($this->trans('commands.config.import.messages.imported')); } }
/** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $io = new DrupalStyle($input, $output); $directory = $input->getArgument('directory'); $source_storage = new FileStorage($directory); if ($input->getOption('reverse')) { $config_comparer = new StorageComparer($source_storage, $this->configStorage, $this->configManager); } else { $config_comparer = new StorageComparer($this->configStorage, $source_storage, $this->configManager); } if (!$config_comparer->createChangelist()->hasChanges()) { $output->writeln($this->trans('commands.config.diff.messages.no-changes')); } $change_list = []; foreach ($config_comparer->getAllCollectionNames() as $collection) { $change_list[$collection] = $config_comparer->getChangelist(null, $collection); } $this->outputDiffTable($io, $change_list); }
/** * Tests config snapshot creation and updating. */ function testSnapshot() { $active = $this->container->get('config.storage'); $sync = $this->container->get('config.storage.sync'); $snapshot = $this->container->get('config.storage.snapshot'); $config_manager = $this->container->get('config.manager'); $config_name = 'config_test.system'; $config_key = 'foo'; $new_data = 'foobar'; $active_snapshot_comparer = new StorageComparer($active, $snapshot, $config_manager); $sync_snapshot_comparer = new StorageComparer($sync, $snapshot, $config_manager); // Verify that we have an initial snapshot that matches the active // configuration. This has to be true as no config should be installed. $this->assertFalse($active_snapshot_comparer->createChangelist()->hasChanges()); // Install the default config. $this->installConfig(array('config_test')); // Although we have imported config this has not affected the snapshot. $this->assertTrue($active_snapshot_comparer->reset()->hasChanges()); // Update the config snapshot. \Drupal::service('config.manager')->createSnapshot($active, $snapshot); // The snapshot and active config should now contain the same config // objects. $this->assertFalse($active_snapshot_comparer->reset()->hasChanges()); // Change a configuration value in sync. $sync_data = $this->config($config_name)->get(); $sync_data[$config_key] = $new_data; $sync->write($config_name, $sync_data); // Verify that active and snapshot match, and that sync doesn't match // active. $this->assertFalse($active_snapshot_comparer->reset()->hasChanges()); $this->assertTrue($sync_snapshot_comparer->createChangelist()->hasChanges()); // Import changed data from sync to active. $this->configImporter()->import(); // Verify changed config was properly imported. \Drupal::configFactory()->reset($config_name); $this->assertIdentical($this->config($config_name)->get($config_key), $new_data); // Verify that a new snapshot was created which and that it matches // the active config. $this->assertFalse($active_snapshot_comparer->reset()->hasChanges()); }
/** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $io = new DrupalStyle($input, $output); $archiveFile = $input->getOption('file'); $directory = $input->getOption('directory'); $removeFiles = $input->getOption('remove-files'); if ($directory) { $configSyncDir = $directory; } else { $configSyncDir = config_get_config_directory(CONFIG_SYNC_DIRECTORY); } // Determine $source_storage in partial and non-partial cases. $active_storage = \Drupal::service('config.storage'); $source_storage = new FileStorage($configSyncDir); /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ $config_manager = \Drupal::service('config.manager'); $storage_comparer = new StorageComparer($source_storage, $active_storage, $config_manager); if (!$storage_comparer->createChangelist()->hasChanges()) { $io->success($this->trans('commands.config.import.messages.nothing-to-do')); } if ($this->configImport($io, $storage_comparer)) { $io->success($this->trans('commands.config.import.messages.imported')); } }
/** * @covers ::createChangelist */ public function testCreateChangelistUpdate() { $target_data = $source_data = $this->getConfigData(); $source_data['system.site']['title'] = 'Drupal New!'; $source_data['field.field.node.article.body']['new_config_key'] = 'new data'; $source_data['field.storage.node.body']['new_config_key'] = 'new data'; $this->sourceStorage->expects($this->once())->method('listAll')->will($this->returnValue(array_keys($source_data))); $this->targetStorage->expects($this->once())->method('listAll')->will($this->returnValue(array_keys($target_data))); $this->sourceStorage->expects($this->once())->method('readMultiple')->will($this->returnValue($source_data)); $this->targetStorage->expects($this->once())->method('readMultiple')->will($this->returnValue($target_data)); $this->sourceStorage->expects($this->once())->method('getAllCollectionNames')->will($this->returnValue(array())); $this->targetStorage->expects($this->once())->method('getAllCollectionNames')->will($this->returnValue(array())); $this->storageComparer->createChangelist(); $expected = array('field.storage.node.body', 'system.site', 'field.field.node.article.body'); $this->assertEquals($expected, $this->storageComparer->getChangelist('update')); $this->assertEmpty($this->storageComparer->getChangelist('create')); $this->assertEmpty($this->storageComparer->getChangelist('delete')); }
/** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $snapshot_comparer = new StorageComparer($this->activeStorage, $this->snapshotStorage, $this->configManager); if (!$form_state->getUserInput() && $snapshot_comparer->createChangelist()->hasChanges()) { $change_list = array(); foreach ($snapshot_comparer->getAllCollectionNames() as $collection) { foreach ($snapshot_comparer->getChangelist(NULL, $collection) as $config_names) { if (empty($config_names)) { continue; } foreach ($config_names as $config_name) { $change_list[] = $config_name; } } } sort($change_list); $change_list_render = array('#theme' => 'item_list', '#items' => $change_list); $change_list_html = drupal_render($change_list_render); drupal_set_message($this->t('Your current configuration has changed. Changes to these configuration items will be lost on the next synchronization: !changes', array('!changes' => $change_list_html)), 'warning'); } $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array('#type' => 'submit', '#value' => $this->t('Import all')); $source_list = $this->stagingStorage->listAll(); $storage_comparer = new StorageComparer($this->stagingStorage, $this->activeStorage, $this->configManager); if (empty($source_list) || !$storage_comparer->createChangelist()->hasChanges()) { $form['no_changes'] = array('#type' => 'table', '#header' => array('Name', 'Operations'), '#rows' => array(), '#empty' => $this->t('There are no configuration changes to import.')); $form['actions']['#access'] = FALSE; return $form; } elseif (!$storage_comparer->validateSiteUuid()) { drupal_set_message($this->t('The staged configuration cannot be imported, because it originates from a different site than this site. You can only synchronize configuration between cloned instances of this site.'), 'error'); $form['actions']['#access'] = FALSE; return $form; } else { // Store the comparer for use in the submit. $form_state->set('storage_comparer', $storage_comparer); } // Add the AJAX library to the form for dialog support. $form['#attached']['library'][] = 'core/drupal.ajax'; foreach ($storage_comparer->getAllCollectionNames() as $collection) { if ($collection != StorageInterface::DEFAULT_COLLECTION) { $form[$collection]['collection_heading'] = array('#type' => 'html_tag', '#tag' => 'h2', '#value' => $this->t('!collection configuration collection', array('!collection' => $collection))); } foreach ($storage_comparer->getChangelist(NULL, $collection) as $config_change_type => $config_names) { if (empty($config_names)) { continue; } // @todo A table caption would be more appropriate, but does not have the // visual importance of a heading. $form[$collection][$config_change_type]['heading'] = array('#type' => 'html_tag', '#tag' => 'h3'); switch ($config_change_type) { case 'create': $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count new', '@count new'); break; case 'update': $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count changed', '@count changed'); break; case 'delete': $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count removed', '@count removed'); break; case 'rename': $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count renamed', '@count renamed'); break; } $form[$collection][$config_change_type]['list'] = array('#type' => 'table', '#header' => array('Name', 'Operations')); foreach ($config_names as $config_name) { if ($config_change_type == 'rename') { $names = $storage_comparer->extractRenameNames($config_name); $route_options = array('source_name' => $names['old_name'], 'target_name' => $names['new_name']); $config_name = $this->t('!source_name to !target_name', array('!source_name' => $names['old_name'], '!target_name' => $names['new_name'])); } else { $route_options = array('source_name' => $config_name); } if ($collection != StorageInterface::DEFAULT_COLLECTION) { $route_name = 'config.diff_collection'; $route_options['collection'] = $collection; } else { $route_name = 'config.diff'; } $links['view_diff'] = array('title' => $this->t('View differences'), 'url' => Url::fromRoute($route_name, $route_options), 'attributes' => array('class' => array('use-ajax'), 'data-accepts' => 'application/vnd.drupal-modal', 'data-dialog-options' => json_encode(array('width' => 700)))); $form[$collection][$config_change_type]['list']['#rows'][] = array('name' => $config_name, 'operations' => array('data' => array('#type' => 'operations', '#links' => $links))); } } } return $form; }
/** * Tests that a fixed set of modules can be installed and uninstalled. */ public function testInstallUninstall() { // Get a list of modules to enable. $all_modules = system_rebuild_module_data(); $all_modules = array_filter($all_modules, function ($module) { // Filter contrib, hidden, already enabled modules and modules in the // Testing package. if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') { return FALSE; } return TRUE; }); // Install every module possible. \Drupal::service('module_installer')->install(array_keys($all_modules)); $this->assertModules(array_keys($all_modules), TRUE); foreach ($all_modules as $module => $info) { $this->assertModuleConfig($module); $this->assertModuleTablesExist($module); } // Export active config to sync. $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); system_list_reset(); $this->resetAll(); // Delete every field on the site so all modules can be uninstalled. For // example, if a comment field exists then module becomes required and can // not be uninstalled. $field_storages = \Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple(); \Drupal::entityManager()->getStorage('field_storage_config')->delete($field_storages); // Purge the data. field_purge_batch(1000); // Delete all terms. $terms = Term::loadMultiple(); entity_delete_multiple('taxonomy_term', array_keys($terms)); // Delete all filter formats. $filters = FilterFormat::loadMultiple(); entity_delete_multiple('filter_format', array_keys($filters)); // Delete any shortcuts so the shortcut module can be uninstalled. $shortcuts = Shortcut::loadMultiple(); entity_delete_multiple('shortcut', array_keys($shortcuts)); system_list_reset(); $all_modules = system_rebuild_module_data(); // Ensure that only core required modules and the install profile can not be uninstalled. $validation_reasons = \Drupal::service('module_installer')->validateUninstall(array_keys($all_modules)); $this->assertEqual(['standard', 'system', 'user'], array_keys($validation_reasons)); $modules_to_uninstall = array_filter($all_modules, function ($module) use($validation_reasons) { // Filter required and not enabled modules. if (!empty($module->info['required']) || $module->status == FALSE) { return FALSE; } return TRUE; }); // Can not uninstall config and use admin/config/development/configuration! unset($modules_to_uninstall['config']); $this->assertTrue(isset($modules_to_uninstall['comment']), 'The comment module will be disabled'); $this->assertTrue(isset($modules_to_uninstall['file']), 'The File module will be disabled'); $this->assertTrue(isset($modules_to_uninstall['editor']), 'The Editor module will be disabled'); // Uninstall all modules that can be uninstalled. \Drupal::service('module_installer')->uninstall(array_keys($modules_to_uninstall)); $this->assertModules(array_keys($modules_to_uninstall), FALSE); foreach ($modules_to_uninstall as $module => $info) { $this->assertNoModuleConfig($module); $this->assertModuleTablesDoNotExist($module); } // Import the configuration thereby re-installing all the modules. $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all')); // Modules have been installed that have services. $this->rebuildContainer(); // Check that there are no errors. $this->assertIdentical($this->configImporter()->getErrors(), array()); // Check that all modules that were uninstalled are now reinstalled. $this->assertModules(array_keys($modules_to_uninstall), TRUE); foreach ($modules_to_uninstall as $module => $info) { $this->assertModuleConfig($module); $this->assertModuleTablesExist($module); } // Ensure that we have no configuration changes to import. $storage_comparer = new StorageComparer($this->container->get('config.storage.sync'), $this->container->get('config.storage'), $this->container->get('config.manager')); $this->assertIdentical($storage_comparer->createChangelist()->getChangelist(), $storage_comparer->getEmptyChangelist()); // Now we have all configuration imported, test all of them for schema // conformance. Ensures all imported default configuration is valid when // all modules are enabled. $names = $this->container->get('config.storage')->listAll(); /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */ $typed_config = $this->container->get('config.typed'); foreach ($names as $name) { $config = $this->config($name); $this->assertConfigSchema($typed_config, $name, $config->get()); } }
/** * Tests that a fixed set of modules can be installed and uninstalled. */ public function testInstallUninstall() { // Get a list of modules to enable. $all_modules = system_rebuild_module_data(); $all_modules = array_filter($all_modules, function ($module) { // Filter hidden, already enabled modules and modules in the Testing // package. if (!empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') { return FALSE; } return TRUE; }); // Install every module possible. \Drupal::moduleHandler()->install(array_keys($all_modules)); $this->assertModules(array_keys($all_modules), TRUE); foreach ($all_modules as $module => $info) { $this->assertModuleConfig($module); $this->assertModuleTablesExist($module); } // Export active config to staging $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); system_list_reset(); $this->resetAll(); // Delete every field on the site so all modules can be uninstalled. For // example, if a comment field exists then module becomes required and can // not be uninstalled. $field_storages = \Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple(); \Drupal::entityManager()->getStorage('field_storage_config')->delete($field_storages); // Purge the data. field_purge_batch(1000); // Delete any forum terms so it can be uninstalled. $vid = \Drupal::config('forum.settings')->get('vocabulary'); $terms = entity_load_multiple_by_properties('taxonomy_term', ['vid' => $vid]); foreach ($terms as $term) { $term->delete(); } system_list_reset(); $all_modules = system_rebuild_module_data(); $modules_to_uninstall = array_filter($all_modules, function ($module) { // Filter required and not enabled modules. if (!empty($module->info['required']) || $module->status == FALSE) { return FALSE; } return TRUE; }); // Can not uninstall config and use admin/config/development/configuration! unset($modules_to_uninstall['config']); $this->assertTrue(isset($modules_to_uninstall['comment']), 'The comment module will be disabled'); // Uninstall all modules that can be uninstalled. \Drupal::moduleHandler()->uninstall(array_keys($modules_to_uninstall)); $this->assertModules(array_keys($modules_to_uninstall), FALSE); foreach ($modules_to_uninstall as $module => $info) { $this->assertNoModuleConfig($module); $this->assertModuleTablesDoNotExist($module); } // Import the configuration thereby re-installing all the modules. $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all')); // Check that there are no errors. $this->assertIdentical($this->configImporter()->getErrors(), array()); // Check that all modules that were uninstalled are now reinstalled. $this->assertModules(array_keys($modules_to_uninstall), TRUE); foreach ($modules_to_uninstall as $module => $info) { $this->assertModuleConfig($module); $this->assertModuleTablesExist($module); } // Ensure that we have no configuration changes to import. $storage_comparer = new StorageComparer($this->container->get('config.storage.staging'), $this->container->get('config.storage'), $this->container->get('config.manager')); $this->assertIdentical($storage_comparer->createChangelist()->getChangelist(), $storage_comparer->getEmptyChangelist()); // Now we have all configuration imported, test all of them for schema // conformance. Ensures all imported default configuration is valid when // all modules are enabled. $names = $this->container->get('config.storage')->listAll(); $factory = $this->container->get('config.factory'); /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */ $typed_config = $this->container->get('config.typed'); foreach ($names as $name) { $config = $factory->get($name); $this->assertConfigSchema($typed_config, $name, $config->get()); } }
/** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { // The confirmation step needs no additional validation. if ($this->data) { return; } // Decode the submitted import. $data = Yaml::decode($form_state->getValue('import')); // Validate for config entities. if ($form_state->getValue('config_type') !== 'system.simple') { $definition = $this->entityManager->getDefinition($form_state->getValue('config_type')); $id_key = $definition->getKey('id'); // If a custom entity ID is specified, override the value in the // configuration data being imported. if (!$form_state->isValueEmpty('custom_entity_id')) { $data[$id_key] = $form_state->getValue('custom_entity_id'); } $entity_storage = $this->entityManager->getStorage($form_state->getValue('config_type')); // If an entity ID was not specified, set an error. if (!isset($data[$id_key])) { $form_state->setErrorByName('import', $this->t('Missing ID key "@id_key" for this @entity_type import.', array('@id_key' => $id_key, '@entity_type' => $definition->getLabel()))); return; } $config_name = $definition->getConfigPrefix() . '.' . $data[$id_key]; // If there is an existing entity, ensure matching ID and UUID. if ($entity = $entity_storage->load($data[$id_key])) { $this->configExists = $entity; if (!isset($data['uuid'])) { $form_state->setErrorByName('import', $this->t('An entity with this machine name already exists but the import did not specify a UUID.')); return; } if ($data['uuid'] !== $entity->uuid()) { $form_state->setErrorByName('import', $this->t('An entity with this machine name already exists but the UUID does not match.')); return; } } elseif (isset($data['uuid']) && $entity_storage->loadByProperties(array('uuid' => $data['uuid']))) { $form_state->setErrorByName('import', $this->t('An entity with this UUID already exists but the machine name does not match.')); } } else { $config_name = $form_state->getValue('config_name'); $config = $this->config($config_name); $this->configExists = !$config->isNew() ? $config : FALSE; } // Use ConfigImporter validation. if (!$form_state->getErrors()) { $source_storage = new StorageReplaceDataWrapper($this->configStorage); $source_storage->replaceData($config_name, $data); $storage_comparer = new StorageComparer($source_storage, $this->configStorage, $this->configManager); if (!$storage_comparer->createChangelist()->hasChanges()) { $form_state->setErrorByName('import', $this->t('There are no changes to import.')); } else { $config_importer = new ConfigImporter($storage_comparer, $this->eventDispatcher, $this->configManager, $this->lock, $this->typedConfigManager, $this->moduleHandler, $this->moduleInstaller, $this->themeHandler, $this->getStringTranslation()); try { $config_importer->validate(); $form_state->set('config_importer', $config_importer); } catch (ConfigImporterException $e) { // There are validation errors. $item_list = ['#theme' => 'item_list', '#items' => $config_importer->getErrors(), '#title' => $this->t('The configuration cannot be imported because it failed validation for the following reasons:')]; $form_state->setErrorByName('import', $this->renderer->render($item_list)); } } } // Store the decoded version of the submitted import. $form_state->setValueForElement($form['import'], $data); }
/** * Constructs the Configuration Sync storage comparer. * * @param \Drupal\Core\Config\StorageInterface $source_storage * Storage object used to read configuration. * @param \Drupal\Core\Config\StorageInterface $target_storage * Storage object used to write configuration. * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager * The configuration manager. * @param \Drupal\config_update\ConfigDiffInterface $config_diff * The config differ. */ public function __construct(StorageInterface $source_storage, StorageInterface $target_storage, ConfigManagerInterface $config_manager, ConfigDiffInterface $config_diff) { parent::__construct($source_storage, $target_storage, $config_manager); $this->configDiff = $config_diff; }