diff --git a/core/modules/config/config.local_tasks.yml b/core/modules/config/config.local_tasks.yml new file mode 100644 index 0000000..027d3f9 --- /dev/null +++ b/core/modules/config/config.local_tasks.yml @@ -0,0 +1,38 @@ +config.sync_tab: + route_name: config.management + title: Synchronize + tab_root_id: config.sync_tab + +config.full_tab: + route_name: config.import + title: 'Full Import/Export' + tab_root_id: config.sync_tab + +config.single_tab: + route_name: config.import_single + title: 'Single Import/Export' + tab_root_id: config.sync_tab + +config.full_export_tab: + route_name: config.export + title: Export + tab_root_id: config.sync_tab + tab_parent_id: config.full_tab + +config.full_import_tab: + route_name: config.import + title: Import + tab_root_id: config.sync_tab + tab_parent_id: config.full_tab + +config.single_export_tab: + route_name: config.export_single + title: Export + tab_root_id: config.sync_tab + tab_parent_id: config.single_tab + +config.single_import_tab: + route_name: config.import_single + title: Import + tab_root_id: config.sync_tab + tab_parent_id: config.single_tab diff --git a/core/modules/config/config.module b/core/modules/config/config.module index 5e93c02..bb9288b 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -64,37 +64,12 @@ function config_menu() { $items['admin/config/development/configuration'] = array( 'title' => 'Configuration management', 'description' => 'Import, export, or synchronize your site configuration.', - 'route_name' => 'config_management', - ); - $items['admin/config/development/configuration/sync'] = array( - 'title' => 'Synchronize', - 'description' => 'Synchronize configuration changes.', - 'route_name' => 'config.sync', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'weight' => 0, - ); - $items['admin/config/development/configuration/export'] = array( - 'title' => 'Export', - 'description' => 'Export your site configuration', - 'route_name' => 'config.export', - 'type' => MENU_LOCAL_TASK, - 'weight' => 1, - ); - $items['admin/config/development/configuration/import'] = array( - 'title' => 'Import', - 'description' => 'Import configuration for your site', - 'route_name' => 'config.import', - 'type' => MENU_LOCAL_TASK, - 'weight' => 2, + 'route_name' => 'config.management', ); $items['admin/config/development/configuration/sync/diff/%'] = array( 'title' => 'Configuration file diff', 'description' => 'Diff between active and staged configuration.', 'route_name' => 'config.diff', ); - $items['admin/config/development/configuration/sync/import'] = array( - 'title' => 'Import', - 'type' => MENU_DEFAULT_LOCAL_TASK, - ); return $items; } diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml index 3eaeb34..9b2229d 100644 --- a/core/modules/config/config.routing.yml +++ b/core/modules/config/config.routing.yml @@ -5,37 +5,58 @@ config.diff: requirements: _permission: 'synchronize configuration' -config_management: +config.management: path: '/admin/config/development/configuration' defaults: _form: '\Drupal\config\Form\ConfigSync' 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' + path: '/admin/config/development/configuration/full/export' defaults: + _title: Export _form: '\Drupal\config\Form\ConfigExportForm' requirements: _permission: 'export configuration' config.import: - path: '/admin/config/development/configuration/import' + path: '/admin/config/development/configuration/full/import' defaults: + _title: Import _form: '\Drupal\config\Form\ConfigImportForm' 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' + config.sync: path: '/admin/config/development/configuration/sync' defaults: + _title: Synchronize _form: '\Drupal\config\Form\ConfigSync' requirements: _permission: 'synchronize configuration' diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigExportForm.php b/core/modules/config/lib/Drupal/config/Form/ConfigExportForm.php index c405c64..dc562dc 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigExportForm.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigExportForm.php @@ -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'; } } 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..4049767 --- /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.staging') + ); + } + + /** + * {@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..10c3af6 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSingleImportForm.php @@ -0,0 +1,239 @@ +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); + } + + $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) { + // 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 { + $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()))); + } + } + +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigExportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigExportUITest.php index 71c6762..45776e5 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigExportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigExportUITest.php @@ -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. diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUploadTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUploadTest.php index b78639b..025c2f4 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUploadTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUploadTest.php @@ -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')); } 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..6d610a0 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSingleImportExportTest.php @@ -0,0 +1,131 @@ + '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 = << '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 = << 'config_test', + 'import' => $import, + ); + $second_uuid = $uuid->generate(); + $edit['import'] .= "\nuuid: " . $second_uuid; + $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' => 'second', '@type' => 'test configuration'))); + $this->drupalPostForm(NULL, array(), t('Confirm')); + $entity = $storage->load('second'); + $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertIdentical($entity->label(), 'Second'); + $this->assertIdentical($entity->id(), 'second'); + $this->assertFalse($entity->status()); + $this->assertIdentical($entity->uuid(), $second_uuid); + } + + /** + * Tests exporting a single configuration file. + */ + public function testExport() { + $this->drupalLogin($this->drupalCreateUser(array('export configuration'))); + + $this->drupalGet('admin/config/development/configuration/single/export/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/single/export/system.simple/system.image'); + $this->assertFieldByXPath('//textarea[@name="export"]', "toolkit: gd\n", 'The expected system configuration is displayed.'); + + $this->drupalGet('admin/config/development/configuration/single/export/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/single/export/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.'); + } + +}