diff --git a/core/modules/config/config.info.yml b/core/modules/config/config.info.yml index 88821ce..3381f65 100644 --- a/core/modules/config/config.info.yml +++ b/core/modules/config/config.info.yml @@ -4,4 +4,4 @@ description: 'Allows administrators to manage configuration changes.' package: Core version: VERSION core: 8.x -configure: config.sync +configure: config.import_full diff --git a/core/modules/config/config.links.menu.yml b/core/modules/config/config.links.menu.yml index 87a9b92..c055d2a 100644 --- a/core/modules/config/config.links.menu.yml +++ b/core/modules/config/config.links.menu.yml @@ -1,5 +1,5 @@ -config.sync: +config.import_full: title: 'Configuration management' - description: 'Import, export, or synchronize your site configuration.' - route_name: config.sync + description: 'Import or export your site configuration.' + route_name: config.import_full parent: system.admin_config_development diff --git a/core/modules/config/config.links.task.yml b/core/modules/config/config.links.task.yml index 5a894f0..02d1fb6 100644 --- a/core/modules/config/config.links.task.yml +++ b/core/modules/config/config.links.task.yml @@ -1,34 +1,29 @@ -config.sync: - route_name: config.sync - base_route: config.sync - title: 'Synchronize' - -config.full: +config.import: route_name: config.import_full - title: 'Full Import/Export' + title: 'Import' base_route: config.sync -config.single: - route_name: config.import_single - title: 'Single Import/Export' +config.export: + route_name: config.export_full + title: 'Export' base_route: config.sync config.export_full: route_name: config.export_full - title: Export - parent_id: config.full + title: Full + parent_id: config.export config.import_full: route_name: config.import_full - title: Import - parent_id: config.full + title: Full + parent_id: config.import config.export_single: route_name: config.export_single - title: Export - parent_id: config.single + title: Single + parent_id: config.export config.import_single: route_name: config.import_single - title: Import - parent_id: config.single + title: Single + parent_id: config.import diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml index 8eebac3..c4f0373 100644 --- a/core/modules/config/config.routing.yml +++ b/core/modules/config/config.routing.yml @@ -2,7 +2,7 @@ config.sync: path: '/admin/config/development/configuration' defaults: _form: '\Drupal\config\Form\ConfigSync' - _title: 'Synchronize' + _title: 'Configuration management' requirements: _permission: 'synchronize configuration' diff --git a/core/modules/config/src/Form/ConfigImportForm.php b/core/modules/config/src/Form/ConfigImportForm.php index 9ed9b83..ab4b1f4 100644 --- a/core/modules/config/src/Form/ConfigImportForm.php +++ b/core/modules/config/src/Form/ConfigImportForm.php @@ -8,10 +8,21 @@ namespace Drupal\config\Form; use Drupal\Core\Archiver\ArchiveTar; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Config\StorageComparer; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Defines the configuration import form. @@ -19,6 +30,13 @@ class ConfigImportForm extends FormBase { /** + * The database lock object. + * + * @var \Drupal\Core\Lock\LockBackendInterface + */ + protected $lock; + + /** * The configuration storage. * * @var \Drupal\Core\Config\StorageInterface @@ -26,13 +44,86 @@ class ConfigImportForm extends FormBase { protected $configStorage; /** + * The staging configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $stagingStorage; + + /** + * The active configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $activeStorage; + + /** + * The snapshot configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; + + /** + * Event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface; + */ + protected $configManager; + + /** + * The typed config manager. + * + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected $typedConfigManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The module installer. + * + * @var \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected $moduleInstaller; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** * Constructs a new ConfigImportForm. * * @param \Drupal\Core\Config\StorageInterface $config_storage * The configuration storage. */ - public function __construct(StorageInterface $config_storage) { + public function __construct(StorageInterface $staging_storage, StorageInterface $active_storage, StorageInterface $snapshot_storage, StorageInterface $config_storage, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config_manager, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler) { + $this->stagingStorage = $staging_storage; + $this->activeStorage = $active_storage; + $this->snapshotStorage = $snapshot_storage; $this->configStorage = $config_storage; + $this->configManager = $config_manager; + $this->eventDispatcher = $event_dispatcher; + $this->lock = $lock; + $this->typedConfigManager = $typed_config_manager; + $this->moduleHandler = $module_handler; + $this->moduleInstaller = $module_installer; + $this->themeHandler = $theme_handler; } /** @@ -40,7 +131,17 @@ public function __construct(StorageInterface $config_storage) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('config.storage.staging') + $container->get('config.storage.staging'), + $container->get('config.storage'), + $container->get('config.storage.snapshot'), + $container->get('config.storage.staging'), + $container->get('config.manager'), + $container->get('event_dispatcher'), + $container->get('lock.persistent'), + $container->get('config.typed'), + $container->get('module_handler'), + $container->get('module_installer'), + $container->get('theme_handler') ); } @@ -61,13 +162,151 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['import_tarball'] = array( '#type' => 'file', '#title' => $this->t('Select your configuration export file'), - '#description' => $this->t('This form will redirect you to the import configuration screen.'), + '#description' => $this->t('Uploaded configuration changes can be reviewed below before they are imported.'), ); $form['submit'] = array( '#type' => 'submit', '#value' => $this->t('Upload'), ); + + $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; + } + + if ($this->snapshotStorage->exists('core.extension')) { + $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('The following items in your active configuration have changes since the last import that may be lost on the next import. !changes', array('!changes' => $change_list_html)), 'warning'); + } + } + + $form['description']['#markup'] .= '

This will replace your previously uploaded configuration.

'; + + // 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'] = $this->formatPlural(count($config_names), '@count new', '@count new'); + break; + + case 'update': + $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(count($config_names), '@count changed', '@count changed'); + break; + + case 'delete': + $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(count($config_names), '@count removed', '@count removed'); + break; + + case 'rename': + $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(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-dialog-type' => '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, + ), + ), + ); + } + } + } + + $form['submit2'] = array( + '#type' => 'submit', + '#value' => $this->t('Import all'), + ); + + return $form; } @@ -75,12 +314,14 @@ public function buildForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { - $file_upload = $this->getRequest()->files->get('files[import_tarball]', NULL, TRUE); - if ($file_upload && $file_upload->isValid()) { - $form_state->setValue('import_tarball', $file_upload->getRealPath()); - } - else { - $form_state->setErrorByName('import_tarball', $this->t('The import tarball could not be uploaded.')); + if ($form_state->getValue('op') == t('Upload')) { + $file_upload = $this->getRequest()->files->get('files[import_tarball]', NULL, TRUE); + if ($file_upload && $file_upload->isValid()) { + $form_state->setValue('import_tarball', $file_upload->getRealPath()); + } + else { + $form_state->setErrorByName('import_tarball', $this->t('The import tarball could not be uploaded.')); + } } } @@ -88,22 +329,117 @@ public function validateForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - if ($path = $form_state->getValue('import_tarball')) { - $this->configStorage->deleteAll(); - try { - $archiver = new ArchiveTar($path, 'gz'); - $files = array(); - foreach ($archiver->listContent() as $file) { - $files[] = $file['filename']; + if ($form_state->getValue('op') == t('Upload')) { + if ($path = $form_state->getValue('import_tarball')) { + $this->configStorage->deleteAll(); + try { + $archiver = new ArchiveTar($path, 'gz'); + $files = array(); + foreach ($archiver->listContent() as $file) { + $files[] = $file['filename']; + } + $archiver->extractList($files, config_get_config_directory(CONFIG_STAGING_DIRECTORY)); + drupal_set_message($this->t('Your configuration files were successfully uploaded, ready for import.')); + $form_state->setRedirect('config.import_full'); + } catch (\Exception $e) { + drupal_set_message($this->t('Could not extract the contents of the tar file. The error message is @message', array('@message' => $e->getMessage())), 'error'); } - $archiver->extractList($files, config_get_config_directory(CONFIG_STAGING_DIRECTORY)); - drupal_set_message($this->t('Your configuration files were successfully uploaded, ready for import.')); - $form_state->setRedirect('config.sync'); + drupal_unlink($path); } - catch (\Exception $e) { - drupal_set_message($this->t('Could not extract the contents of the tar file. The error message is @message', array('@message' => $e->getMessage())), 'error'); + } + else { + $config_importer = new ConfigImporter( + $form_state->get('storage_comparer'), + $this->eventDispatcher, + $this->configManager, + $this->lock, + $this->typedConfigManager, + $this->moduleHandler, + $this->moduleInstaller, + $this->themeHandler, + $this->getStringTranslation() + ); + if ($config_importer->alreadyImporting()) { + drupal_set_message($this->t('Another request may be synchronizing configuration already.')); + } + else{ + try { + $sync_steps = $config_importer->initialize(); + $batch = array( + 'operations' => array(), + 'finished' => array(get_class($this), 'finishBatch'), + 'title' => t('Synchronizing configuration'), + 'init_message' => t('Starting configuration synchronization.'), + 'progress_message' => t('Completed @current step of @total.'), + 'error_message' => t('Configuration synchronization has encountered an error.'), + 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', + ); + foreach ($sync_steps as $sync_step) { + $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step)); + } + + batch_set($batch); + } + catch (ConfigImporterException $e) { + // There are validation errors. + drupal_set_message($this->t('The configuration cannot be imported because it failed validation for the following reasons:'), 'error'); + foreach ($config_importer->getErrors() as $message) { + drupal_set_message($message, 'error'); + } + } + } + } + } + /** + * Processes the config import batch and persists the importer. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The batch config importer object to persist. + * @param string $sync_step + * The synchronization step to do. + * @param $context + * The batch context. + */ + public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) { + if (!isset($context['sandbox']['config_importer'])) { + $context['sandbox']['config_importer'] = $config_importer; + } + + $config_importer = $context['sandbox']['config_importer']; + $config_importer->doSyncStep($sync_step, $context); + if ($errors = $config_importer->getErrors()) { + if (!isset($context['results']['errors'])) { + $context['results']['errors'] = array(); + } + $context['results']['errors'] += $errors; + } + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + */ + public static function finishBatch($success, $results, $operations) { + if ($success) { + if (!empty($results['errors'])) { + foreach ($results['errors'] as $error) { + drupal_set_message($error, 'error'); + \Drupal::logger('config_sync')->error($error); + } + drupal_set_message(\Drupal::translation()->translate('The configuration was imported with errors.'), 'warning'); } - drupal_unlink($path); + else { + drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.')); + } + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE))); + drupal_set_message($message, 'error'); } } diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml index a9072ca..c44c73b 100644 --- a/sites/default/default.services.yml +++ b/sites/default/default.services.yml @@ -51,7 +51,7 @@ parameters: # changes (see auto_reload below). # # For more information about debugging Twig templates, see - # https://www.drupal.org/node/1906392. + # http://drupal.org/node/1906392. # # Not recommended in production environments # @default false diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index ef83939..3783d0a 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -22,7 +22,7 @@ * 'sites/default' will be used. * * For example, for a fictitious site installed at - * https://www.drupal.org:8080/mysite/test/, the 'settings.php' file is searched + * http://www.drupal.org:8080/mysite/test/, the 'settings.php' file is searched * for in the following directories: * * - sites/8080.www.drupal.org.mysite.test @@ -44,7 +44,7 @@ * * Note that if you are installing on a non-standard port number, prefix the * hostname with that number. For example, - * https://www.drupal.org:8080/mysite/test/ could be loaded from + * http://www.drupal.org:8080/mysite/test/ could be loaded from * sites/8080.www.drupal.org.mysite.test/. * * @see example.sites.php @@ -430,7 +430,7 @@ * the code directly via SSH or FTP themselves. This setting completely * disables all functionality related to these authorized file operations. * - * @see https://www.drupal.org/node/244924 + * @see http://drupal.org/node/244924 * * Remove the leading hash signs to disable. */ @@ -463,8 +463,8 @@ * Note: Caches need to be cleared when this value is changed to make the * private:// stream wrapper available to the system. * - * See https://www.drupal.org/documentation/modules/file for more information - * about securing private files. + * See http://drupal.org/documentation/modules/file for more information about + * securing private files. */ # $settings['file_private_path'] = '';