Commit a42b392c authored by webchick's avatar webchick

Issue #2099363 by tim.plunkett, mtift, swentel: Allow single config files to...

Issue #2099363 by tim.plunkett, mtift, swentel: Allow single config files to be imported and exported (Resolve regression from Views in Drupal 7).
parent 0e0bd627
......@@ -2,14 +2,37 @@ config.sync:
route_name: config.sync
tab_root_id: config.sync
title: 'Synchronize'
weight: 0
config.export:
route_name: config.export
config.full:
route_name: config.import_full
title: 'Full Import/Export'
tab_root_id: config.sync
config.single:
route_name: config.import_single
title: 'Single Import/Export'
tab_root_id: config.sync
config.export_full:
route_name: config.export_full
title: Export
tab_root_id: config.sync
tab_parent_id: config.full
config.import_full:
route_name: config.import_full
title: Import
tab_root_id: config.sync
tab_parent_id: config.full
config.export_single:
route_name: config.export_single
title: Export
weight: 1
config.import:
route_name: config.import
tab_root_id: config.sync
tab_parent_id: config.single
config.import_single:
route_name: config.import_single
title: Import
tab_root_id: config.sync
weight: 2
tab_parent_id: config.single
......@@ -13,25 +13,43 @@ config.diff:
requirements:
_permission: 'synchronize configuration'
config_export_download:
path: '/admin/config/development/configuration/export-download'
config.export_download:
path: '/admin/config/development/configuration/full/export-download'
defaults:
_controller: '\Drupal\config\Controller\ConfigController::downloadExport'
requirements:
_permission: 'export configuration'
config.export:
path: '/admin/config/development/configuration/export'
config.export_full:
path: '/admin/config/development/configuration/full/export'
defaults:
_form: '\Drupal\config\Form\ConfigExportForm'
_title: 'Export'
requirements:
_permission: 'export configuration'
config.import:
path: '/admin/config/development/configuration/import'
config.import_full:
path: '/admin/config/development/configuration/full/import'
defaults:
_form: '\Drupal\config\Form\ConfigImportForm'
_title: 'Import'
requirements:
_permission: 'import configuration'
config.import_single:
path: '/admin/config/development/configuration/single/import'
defaults:
_title: 'Single import'
_form: '\Drupal\config\Form\ConfigSingleImportForm'
requirements:
_permission: 'import configuration'
config.export_single:
path: '/admin/config/development/configuration/single/export/{config_type}/{config_name}'
defaults:
_title: 'Single export'
_form: '\Drupal\config\Form\ConfigSingleExportForm'
config_type: NULL
config_name: NULL
requirements:
_permission: 'export configuration'
......@@ -39,7 +39,7 @@ public function buildForm(array $form, array &$form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
$form_state['redirect'] = 'admin/config/development/configuration/export-download';
$form_state['redirect'] = 'admin/config/development/configuration/full/export-download';
}
}
<?php
/**
* @file
* Contains \Drupal\config\Form\ConfigSingleExportForm.
*/
namespace Drupal\config\Form;
use Drupal\Component\Utility\MapArray;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Form\FormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for exporting a single configuration file.
*/
class ConfigSingleExportForm extends FormBase {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* The config storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* Tracks the valid config entity type definitions.
*
* @var array
*/
protected $definitions = array();
/**
* Constructs a new ConfigSingleImportForm.
*
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The config storage.
*/
public function __construct(EntityManager $entity_manager, StorageInterface $config_storage) {
$this->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;
}
}
$entity_types = array_map(function ($definition) {
return $definition['label'];
}, $this->definitions);
// Sort the entity types by label, then add the simple config to the top.
uasort($entity_types, 'strnatcasecmp');
$config_types = array(
'system.simple' => $this->t('Simple configuration'),
) + $entity_types;
$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' => '<div id="edit-config-type-wrapper">',
'#suffix' => '</div>',
'#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' => '<div id="edit-export-wrapper">',
'#suffix' => '</div>',
);
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.
}
}
<?php
/**
* @file
* Contains \Drupal\config\Form\ConfigSingleImportForm.
*/
namespace Drupal\config\Form;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Form\ConfirmFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for importing a single configuration file.
*/
class ConfigSingleImportForm extends ConfirmFormBase {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* The config storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* If the config exists, this is that object. Otherwise, FALSE.
*
* @var \Drupal\Core\Config\Config|\Drupal\Core\Config\Entity\ConfigEntityInterface|bool
*/
protected $configExists = FALSE;
/**
* The submitted data needing to be confirmed.
*
* @var array
*/
protected $data = array();
/**
* Constructs a new ConfigSingleImportForm.
*
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The config storage.
*/
public function __construct(EntityManager $entity_manager, StorageInterface $config_storage) {
$this->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 getCancelRoute() {
return array(
'route_name' => 'config.import_single',
);
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
if ($this->data['config_type'] === 'system.simple') {
$name = $this->data['config_name'];
$type = $this->t('Simple configuration');
}
else {
$definition = $this->entityManager->getDefinition($this->data['config_type']);
$name = $this->data['import'][$definition['entity_keys']['id']];
$type = $definition['label'];
}
$args = array(
'%name' => $name,
'@type' => strtolower($type),
);
if ($this->configExists) {
$question = $this->t('Are you sure you want to update the %name @type?', $args);
}
else {
$question = $this->t('Are you sure you want to create new %name @type?', $args);
}
return $question;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, array &$form_state) {
// When this is the confirmation step fall through to the confirmation form.
if ($this->data) {
return parent::buildForm($form, $form_state);
}
$entity_types = array();
foreach ($this->entityManager->getDefinitions() as $entity_type => $definition) {
if (isset($definition['config_prefix']) && isset($definition['entity_keys']['uuid'])) {
$entity_types[$entity_type] = $definition['label'];
}
}
// Sort the entity types by label, then add the simple config to the top.
uasort($entity_types, 'strnatcasecmp');
$config_types = array(
'system.simple' => $this->t('Simple configuration'),
) + $entity_types;
$form['config_type'] = array(
'#title' => $this->t('Configuration type'),
'#type' => 'select',
'#options' => $config_types,
'#required' => TRUE,
);
$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) {
// The confirmation step needs no additional validation.
if ($this->data) {
return;
}
// 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])) {
$this->configExists = $entity;
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.'));
}
}
else {
$config = $this->config($form_state['values']['config_name']);
$this->configExists = $config->isNew() ? $config : FALSE;
}
// Store the decoded version of the submitted import.
form_set_value($form['import'], $data, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
// If this form has not yet been confirmed, store the values and rebuild.
if (!$this->data) {
$form_state['rebuild'] = TRUE;
$this->data = $form_state['values'];
return;
}
// If a simple configuration file was added, set the data and save.
if ($this->data['config_type'] === 'system.simple') {
$this->config($this->data['config_name'])->setData($this->data['import'])->save();
drupal_set_message($this->t('The %name configuration was imported.', array('%name' => $this->data['config_name'])));
}
// For a config entity, create a new entity and save it.
else {
try {
$entity = $this->entityManager
->getStorageController($this->data['config_type'])
->create($this->data['import']);
$entity->save();
drupal_set_message($this->t('The @entity_type %label was imported.', array('@entity_type' => $entity->entityType(), '%label' => $entity->label())));
}
catch (\Exception $e) {
drupal_set_message($e->getMessage(), 'error');
}
}
}
}
......@@ -75,7 +75,7 @@ function testExport() {
\Drupal::config('system.site')
->set('slogan', $this->slogan)
->save();
$this->drupalPostForm('admin/config/development/configuration/export', array(), 'Export');
$this->drupalPostForm('admin/config/development/configuration/full/export', array(), 'Export');
$this->tarball = $this->drupalGetContent();
}
......@@ -105,7 +105,7 @@ function testImport() {
*/
protected function doImport($filename) {
$this->assertNotEqual($this->slogan, \Drupal::config('system.site')->get('slogan'));
$this->drupalPostForm('admin/config/development/configuration/import', array('files[import_tarball]' => $filename), 'Upload');
$this->drupalPostForm('admin/config/development/configuration/full/import', array('files[import_tarball]' => $filename), 'Upload');
$this->drupalPostForm(NULL, array(), 'Import all');
$this->assertEqual($this->slogan, \Drupal::config('system.site')->get('slogan'));
}
......
......@@ -44,11 +44,11 @@ protected function setUp() {
*/
function testExport() {
// Verify the export page with export submit button is available.
$this->drupalGet('admin/config/development/configuration/export');
$this->drupalGet('admin/config/development/configuration/full/export');
$this->assertFieldById('edit-submit', t('Export'));
// Submit the export form and verify response.
$this->drupalPostForm('admin/config/development/configuration/export', array(), t('Export'));
$this->drupalPostForm('admin/config/development/configuration/full/export', array(), t('Export'));
$this->assertResponse(200, 'User can access the download callback.');
// Get the archived binary file provided to user for download.
......
......@@ -36,13 +36,13 @@ function setUp() {
*/
function testImport() {
// Verify access to the config upload form.
$this->drupalGet('admin/config/development/configuration/import');
$this->drupalGet('admin/config/development/configuration/full/import');
$this->assertResponse(200);
// Attempt to upload a non-tar file.
$text_file = current($this->drupalGetTestFiles('text'));
$edit = array('files[import_tarball]' => drupal_realpath($text_file->uri));
$this->drupalPostForm('admin/config/development/configuration/import', $edit, t('Upload'));
$this->drupalPostForm('admin/config/development/configuration/full/import', $edit, t('Upload'));
$this->assertText(t('Could not extract the contents of the tar file'));
}
......
<?php
/**
* @file
* Contains \Drupal\config\Tests\ConfigSingleImportExportTest.
*/
namespace Drupal\config\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the user interface for importing/exporting a single configuration.
*/
class ConfigSingleImportExportTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('config', 'config_test');
public static function getInfo() {
return array(
'name' => '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() {
$storage = \Drupal::entityManager()->getStorageController('config_test');
$uuid = \Drupal::service('uuid');
$this->drupalLogin($this->drupalCreateUser(array('import configuration')));
$import = <<<EOD
label: First
weight: 0
style: ''
status: '1'
EOD;
$edit = array(
'config_type' => 'config_test',
'import' => $import,
);
// Attempt an import with a missing ID.
$this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import'));
$this->assertText(t('Missing ID key "@id_key" for this @entity_type import.', array('@id_key' => 'id', '@entity_type' => 'Test configuration')));
// Perform an import with no specified UUID and a unique ID.
$this->assertNull($storage->load('first'));
$edit['import'] = "id: first\n" . $edit['import'];
$this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import'));
$this->assertRaw(t('Are you sure you want to create new %name @type?', array('%name' => 'first', '@type' => 'test configuration')));
$this->drupalPostForm(NULL, array(), t('Confirm'));
$entity = $storage->load('first');
$this->assertIdentical($entity->label(), 'First');
$this->assertIdentical($entity->id(), 'first');
$this->assertTrue($entity->status());
$this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label())));
// Attempt an import with an existing ID but missing UUID.
$this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import'));
$this->assertText(t('An entity with this machine name already exists but the import did not specify a UUID.'));
// Attempt an import with a mismatched UUID and existing ID.
$edit['import'] .= "\nuuid: " . $uuid->generate();
$this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import'));
$this->assertText(t('An entity with this machine name already exists but the UUID does not match.'));
// Perform an import with a unique ID and UUID.
$import = <<<EOD
id: second
label: Second
weight: 0
style: ''
status: '0'
EOD;
$edit = array(
'config_type' => 'config_test',
'import' => $import,
);