diff --git a/core/modules/config/config.module b/core/modules/config/config.module
index 5e93c02..bad0582 100644
--- a/core/modules/config/config.module
+++ b/core/modules/config/config.module
@@ -87,6 +87,20 @@ function config_menu() {
'type' => MENU_LOCAL_TASK,
'weight' => 2,
);
+ $items['admin/config/development/configuration/export-single'] = array(
+ 'title' => 'Export single config',
+ 'description' => 'Export a single configuration file for your site',
+ 'route_name' => 'config.export_single',
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 15,
+ );
+ $items['admin/config/development/configuration/import-single'] = array(
+ 'title' => 'Import single config',
+ 'description' => 'Import a single configuration file for your site',
+ 'route_name' => 'config.import_single',
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 20,
+ );
$items['admin/config/development/configuration/sync/diff/%'] = array(
'title' => 'Configuration file diff',
'description' => 'Diff between active and staged configuration.',
diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml
index 3eaeb34..67c0624 100644
--- a/core/modules/config/config.routing.yml
+++ b/core/modules/config/config.routing.yml
@@ -33,6 +33,22 @@ config.import:
requirements:
_permission: 'import configuration'
+config.import_single:
+ path: '/admin/config/development/configuration/import-single'
+ defaults:
+ _form: '\Drupal\config\Form\ConfigSingleImportForm'
+ requirements:
+ _permission: 'import configuration'
+
+config.export_single:
+ path: '/admin/config/development/configuration/export-single/{config_type}/{config_name}'
+ defaults:
+ _form: '\Drupal\config\Form\ConfigSingleExportForm'
+ config_type: NULL
+ config_name: NULL
+ requirements:
+ _permission: 'export configuration'
+
config.sync:
path: '/admin/config/development/configuration/sync'
defaults:
diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSingleExportForm.php b/core/modules/config/lib/Drupal/config/Form/ConfigSingleExportForm.php
new file mode 100644
index 0000000..21a5b57
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Form/ConfigSingleExportForm.php
@@ -0,0 +1,201 @@
+entityManager = $entity_manager;
+ $this->configStorage = $config_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager'),
+ $container->get('config.storage')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormID() {
+ return 'config_single_export_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state, $config_type = NULL, $config_name = NULL) {
+ foreach ($this->entityManager->getDefinitions() as $entity_type => $definition) {
+ if (isset($definition['config_prefix']) && isset($definition['entity_keys']['uuid'])) {
+ $this->definitions[$entity_type] = $definition;
+ }
+ }
+ $config_types = array(
+ 'system.simple' => $this->t('Simple configuration'),
+ );
+ $config_types += array_map(function ($definition) {
+ return $definition['label'];
+ }, $this->definitions);
+ $form['config_type'] = array(
+ '#title' => $this->t('Configuration type'),
+ '#type' => 'select',
+ '#options' => $config_types,
+ '#default_value' => $config_type,
+ '#ajax' => array(
+ 'callback' => array($this, 'updateConfigurationType'),
+ 'wrapper' => 'edit-config-type-wrapper',
+ ),
+ );
+ $default_type = isset($form_state['values']['config_type']) ? $form_state['values']['config_type'] : $config_type;
+ $form['config_name'] = array(
+ '#title' => $this->t('Configuration name'),
+ '#type' => 'select',
+ '#options' => $this->findConfiguration($default_type),
+ '#default_value' => $config_name,
+ '#required' => TRUE,
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#ajax' => array(
+ 'callback' => array($this, 'updateExport'),
+ 'wrapper' => 'edit-export-wrapper',
+ ),
+ );
+
+ $form['export'] = array(
+ '#title' => $this->t('Here is your configuration:'),
+ '#type' => 'textarea',
+ '#rows' => 24,
+ '#required' => TRUE,
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ if ($config_type && $config_name) {
+ $fake_form_state = array('values' => array(
+ 'config_type' => $config_type,
+ 'config_name' => $config_name,
+ ));
+ $form['export'] = $this->updateExport($form, $fake_form_state);
+ }
+ return $form;
+ }
+
+ /**
+ * Handles switching the configuration type selector.
+ */
+ public function updateConfigurationType($form, &$form_state) {
+ $form['config_name']['#options'] = $this->findConfiguration($form_state['values']['config_type']);
+ return $form['config_name'];
+ }
+
+ /**
+ * Handles switching the export textarea.
+ */
+ public function updateExport($form, &$form_state) {
+ // Determine the full config name for the selected config entity.
+ if ($form_state['values']['config_type'] !== 'system.simple') {
+ $definition = $this->entityManager->getDefinition($form_state['values']['config_type']);
+ $name = $definition['config_prefix'] . '.' . $form_state['values']['config_name'];
+ }
+ // The config name is used directly for simple configuration.
+ else {
+ $name = $form_state['values']['config_name'];
+ }
+ // Read the raw data for this config name, encode it, and display it.
+ $data = $this->configStorage->read($name);
+ $form['export']['#value'] = $this->configStorage->encode($data);
+ $form['export']['#description'] = $this->t('The filename is %name.', array('%name' => $name . '.yml'));
+ return $form['export'];
+ }
+
+ /**
+ * Handles switching the configuration type selector.
+ */
+ protected function findConfiguration($config_type) {
+ $names = array(
+ '' => $this->t('- Select -'),
+ );
+ // For a given entity type, load all entities.
+ if ($config_type && $config_type !== 'system.simple') {
+ $entity_storage = $this->entityManager->getStorageController($config_type);
+ foreach ($entity_storage->loadMultiple() as $entity) {
+ $entity_id = $entity->id();
+ $label = $entity->label() ?: $entity_id;
+ $names[$entity_id] = $label;
+ }
+ }
+ // Handle simple configuration.
+ else {
+ // Gather the config entity prefixes.
+ $config_prefixes = array_map(function ($definition) {
+ return $definition['config_prefix'] . '.';
+ }, $this->definitions);
+
+ // Find all config, and then filter our anything matching a config prefix.
+ $names = MapArray::copyValuesToKeys($this->configStorage->listAll());
+ foreach ($names as $config_name) {
+ foreach ($config_prefixes as $config_prefix) {
+ if (strpos($config_name, $config_prefix) === 0) {
+ unset($names[$config_name]);
+ }
+ }
+ }
+ }
+ return $names;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ // Nothing to submit.
+ }
+
+}
diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSingleImportForm.php b/core/modules/config/lib/Drupal/config/Form/ConfigSingleImportForm.php
new file mode 100644
index 0000000..e45272c
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Form/ConfigSingleImportForm.php
@@ -0,0 +1,167 @@
+entityManager = $entity_manager;
+ $this->configStorage = $config_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager'),
+ $container->get('config.storage')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormID() {
+ return 'config_single_import_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state) {
+ $config_types = array(
+ 'system.simple' => $this->t('Simple configuration'),
+ );
+ foreach ($this->entityManager->getDefinitions() as $entity_type => $definition) {
+ if (isset($definition['config_prefix']) && isset($definition['entity_keys']['uuid'])) {
+ $config_types[$entity_type] = $definition['label'];
+ }
+ }
+ $form['config_type'] = array(
+ '#title' => $this->t('Configuration type'),
+ '#type' => 'select',
+ '#options' => $config_types,
+ );
+ $form['config_name'] = array(
+ '#title' => $this->t('Configuration name'),
+ '#type' => 'textfield',
+ '#states' => array(
+ 'required' => array(
+ ':input[name="config_type"]' => array('value' => 'system.simple'),
+ ),
+ 'visible' => array(
+ ':input[name="config_type"]' => array('value' => 'system.simple'),
+ ),
+ ),
+ );
+ $form['import'] = array(
+ '#title' => $this->t('Paste your configuration here'),
+ '#type' => 'textarea',
+ '#rows' => 24,
+ '#required' => TRUE,
+ );
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => $this->t('Import'),
+ '#button_type' => 'primary',
+ );
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, array &$form_state) {
+ // Decode the submitted import.
+ $data = $this->configStorage->decode($form_state['values']['import']);
+
+ // Validate for config entities.
+ if ($form_state['values']['config_type'] !== 'system.simple') {
+ $definition = $this->entityManager->getDefinition($form_state['values']['config_type']);
+ $id_key = $definition['entity_keys']['id'];
+ $entity_storage = $this->entityManager->getStorageController($form_state['values']['config_type']);
+ // If an entity ID was not specified, set an error.
+ if (!isset($data[$id_key])) {
+ form_set_error('import', $this->t('Missing ID key "@id_key" for this @entity_type import.', array('@id_key' => $id_key, '@entity_type' => $definition['label'])));
+ return;
+ }
+ $uuid_key = $definition['entity_keys']['uuid'];
+ // If there is an existing entity, ensure matching ID and UUID.
+ if ($entity = $entity_storage->load($data[$id_key])) {
+ if (!isset($data[$uuid_key])) {
+ form_set_error('import', $this->t('An entity with this machine name already exists but the import did not specify a UUID.'));
+ return;
+ }
+ if ($data[$uuid_key] !== $entity->uuid()) {
+ form_set_error('import', $this->t('An entity with this machine name already exists but the UUID does not match.'));
+ return;
+ }
+ }
+ // If there is no entity with a matching ID, check for a UUID match.
+ elseif (isset($data[$uuid_key]) && $entity_storage->loadByProperties(array($uuid_key => $data[$uuid_key]))) {
+ form_set_error('import', $this->t('An entity with this UUID already exists but the machine name does not match.'));
+ }
+ }
+
+ // Replace the submitted import with the decoded version.
+ form_set_value($form['import'], $data, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ // If a simple configuration file was added, set the data and save.
+ if ($form_state['values']['config_type'] === 'system.simple') {
+ $this->config($form_state['values']['config_name'])->setData($form_state['values']['import'])->save();
+ drupal_set_message($this->t('The %name configuration was imported.', array('%name' => $form_state['values']['config_name'])));
+ }
+ // For a config entity, create a new entity and save it.
+ else {
+ $entity = $this->entityManager
+ ->getStorageController($form_state['values']['config_type'])
+ ->create($form_state['values']['import']);
+ $entity->save();
+
+ drupal_set_message($this->t('The @entity_type %label was imported.', array('@entity_type' => $entity->entityType(), '%label' => $entity->label())));
+ }
+ }
+
+}
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSingleImportExportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSingleImportExportTest.php
new file mode 100644
index 0000000..7798e19
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSingleImportExportTest.php
@@ -0,0 +1,96 @@
+ 'Configuration Single Import/Export UI',
+ 'description' => 'Tests the user interface for importing/exporting a single configuration.',
+ 'group' => 'Configuration',
+ );
+ }
+
+ /**
+ * Tests importing a single configuration file.
+ */
+ public function testImport() {
+ $this->drupalLogin($this->drupalCreateUser(array('import configuration')));
+ $import = << 'config_test',
+ 'import' => $import,
+ );
+ $this->drupalPostForm('admin/config/development/configuration/import-single', $edit, t('Import'));
+ $this->assertText(t('Missing ID key "@id_key" for this @entity_type import.', array('@id_key' => 'id', '@entity_type' => 'Test configuration')));
+
+ $edit['import'] = "id: new\n" . $edit['import'];
+ $this->drupalPostForm('admin/config/development/configuration/import-single', $edit, t('Import'));
+ $entity = entity_load('config_test', 'new');
+ $this->assertEqual($entity->label(), 'New');
+ $this->assertTrue($entity->status());
+ $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label())));
+
+ $this->drupalPostForm('admin/config/development/configuration/import-single', $edit, t('Import'));
+ $this->assertText(t('An entity with this machine name already exists but the import did not specify a UUID.'));
+
+ $edit['import'] .= "\nuuid: " . \Drupal::service('uuid')->generate();
+ $this->drupalPostForm('admin/config/development/configuration/import-single', $edit, t('Import'));
+ $this->assertText(t('An entity with this machine name already exists but the UUID does not match.'));
+ }
+
+ /**
+ * Tests exporting a single configuration file.
+ */
+ public function testExport() {
+ $this->drupalLogin($this->drupalCreateUser(array('export configuration')));
+
+ $this->drupalGet('admin/config/development/configuration/export-single/system.simple');
+ $this->assertFieldByXPath('//select[@name="config_type"]//option', t('Date format'), 'The date format entity type is selected when specified in the URL.');
+ // Spot check several known simple configuration files.
+ $element = $this->xpath('//select[@name="config_name"]');
+ $options = $this->getAllOptions($element[0]);
+ $expected_options = array('filter.settings', 'system.site', 'user.settings');
+ foreach ($options as &$option) {
+ $option = (string) $option;
+ }
+ $this->assertIdentical($expected_options, array_intersect($expected_options, $options), 'The expected configuration files are listed.');
+
+ $this->drupalGet('admin/config/development/configuration/export-single/system.simple/system.image');
+ $this->assertFieldByXPath('//textarea[@name="export"]', "toolkit: gd\n", 'The expected system configuration is displayed.');
+
+ $this->drupalGet('admin/config/development/configuration/export-single/date_format');
+ $this->assertFieldByXPath('//select[@name="config_type"]//option', t('Date format'), 'The date format entity type is selected when specified in the URL.');
+
+ $this->drupalGet('admin/config/development/configuration/export-single/date_format/fallback');
+ $this->assertFieldByXPath('//select[@name="config_name"]//option', t('Fallback date format'), 'The fallback date format config entity is selected when specified in the URL.');
+
+ $fallback_date = \Drupal::entityManager()->getStorageController('date_format')->load('fallback');
+ $data = \Drupal::service('config.storage')->encode($fallback_date->getExportProperties());
+ $this->assertFieldByXPath('//textarea[@name="export"]', $data, 'The fallback date format config entity export code is displayed.');
+ }
+
+}