3 namespace Drupal\migrate_drupal_ui\Form;
5 use Drupal\Core\Datetime\DateFormatterInterface;
6 use Drupal\Core\Form\ConfirmFormBase;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Render\RendererInterface;
9 use Drupal\Core\State\StateInterface;
11 use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
12 use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch;
13 use Drupal\migrate_drupal\MigrationConfigurationTrait;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
17 * Defines a multi-step form for performing direct site upgrades.
19 class MigrateUpgradeForm extends ConfirmFormBase {
21 use MigrationConfigurationTrait;
26 * @var \Drupal\Core\State\StateInterface
31 * The date formatter service.
33 * @var \Drupal\Core\Datetime\DateFormatterInterface
35 protected $dateFormatter;
38 * The renderer service.
40 * @var \Drupal\Core\Render\RendererInterface
45 * The migration plugin manager.
47 * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
49 protected $pluginManager;
52 * Constructs the MigrateUpgradeForm.
54 * @param \Drupal\Core\State\StateInterface $state
56 * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
57 * The date formatter service.
58 * @param \Drupal\Core\Render\RendererInterface $renderer
59 * The renderer service.
60 * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $plugin_manager
61 * The migration plugin manager.
63 public function __construct(StateInterface $state, DateFormatterInterface $date_formatter, RendererInterface $renderer, MigrationPluginManagerInterface $plugin_manager) {
64 $this->state = $state;
65 $this->dateFormatter = $date_formatter;
66 $this->renderer = $renderer;
67 $this->pluginManager = $plugin_manager;
73 public static function create(ContainerInterface $container) {
75 $container->get('state'),
76 $container->get('date.formatter'),
77 $container->get('renderer'),
78 $container->get('plugin.manager.migration')
85 public function getFormId() {
86 return 'migrate_drupal_ui_form';
92 public function buildForm(array $form, FormStateInterface $form_state) {
93 $step = $form_state->get('step') ?: 'overview';
96 return $this->buildOverviewForm($form, $form_state);
99 return $this->buildCredentialForm($form, $form_state);
102 return $this->buildConfirmForm($form, $form_state);
105 drupal_set_message($this->t('Unrecognized form step @step', ['@step' => $step]), 'error');
113 public function submitForm(array &$form, FormStateInterface $form_state) {
114 // This method is intentionally empty, see the specific submit methods for
119 * Builds the form presenting an overview of the migration process.
122 * An associative array containing the structure of the form.
123 * @param \Drupal\Core\Form\FormStateInterface $form_state
124 * The current state of the form.
127 * The form structure.
129 public function buildOverviewForm(array $form, FormStateInterface $form_state) {
130 $form['#title'] = $this->t('Upgrade');
132 if ($date_performed = $this->state->get('migrate_drupal_ui.performed')) {
133 // @todo Add back support for rollbacks and incremental migrations.
134 // https://www.drupal.org/node/2687843
135 // https://www.drupal.org/node/2687849
136 $form['upgrade_option_item'] = [
138 '#prefix' => $this->t('An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal 8. Rollbacks and incremental migrations are not yet supported through the user interface. For more information, see the <a href=":url">upgrading handbook</a>.', [':url' => 'https://www.drupal.org/upgrade/migrate']),
139 '#description' => $this->t('Last upgrade: @date', ['@date' => $this->dateFormatter->format($date_performed)]),
144 $form['info_header'] = [
145 '#markup' => '<p>' . $this->t('Upgrade a site by importing its database and files into a clean and empty new install of Drupal 8. See the <a href=":url">Drupal site upgrades handbook</a> for more information.', [
146 ':url' => 'https://www.drupal.org/upgrade/migrate',
150 $legend[] = $this->t('<em>Old site:</em> the site you want to upgrade.');
151 $legend[] = $this->t('<em>New site:</em> this empty Drupal 8 installation you will import the old site to.');
154 '#theme' => 'item_list',
155 '#title' => $this->t('Definitions'),
156 '#list_type' => 'ul',
160 $info[] = $this->t('You may need multiple tries for a successful upgrade so <strong>backup the database</strong> for this new site. The upgrade will change it and you may want to revert to its initial state.');
161 $info[] = $this->t('Make sure that <strong>access to the database</strong> for the old site is available from this new site.');
162 $info[] = $this->t('<strong>If the old site has private files</strong>, a copy of its files directory must also be accessible on the host of this new site.');
163 $info[] = $this->t('<strong>Enable all modules on this new site</strong> that are enabled on the old site. For example, if the old site uses the book module, then enable the book module on this new site so that the existing data can be imported to it.');
164 $info[] = $this->t('<strong>Do not add any content to the new site</strong> before upgrading. Any existing content is likely to be overwritten by the upgrade process. See <a href=":url">the upgrade preparation guide</a>.', [
165 ':url' => 'https://www.drupal.org/docs/8/upgrade/preparing-an-upgrade#dont_create_content',
167 $info[] = $this->t('Put this site into <a href=":url">maintenance mode</a>.', [
168 ':url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
172 '#theme' => 'item_list',
173 '#title' => $this->t('Steps to prepare for the upgrade'),
174 '#list_type' => 'ol',
178 $form['info_footer'] = [
179 '#markup' => '<p>' . $this->t('The upgrade can take a long time. It is better to upgrade from a local copy of your site instead of directly from your live site.'),
185 $form['actions'] = ['#type' => 'actions'];
186 $form['actions']['save'] = [
188 '#value' => $this->t('Continue'),
189 '#button_type' => 'primary',
190 '#validate' => $validate,
191 '#submit' => ['::submitOverviewForm'],
197 * Form submission handler for the overview form.
200 * An associative array containing the structure of the form.
201 * @param \Drupal\Core\Form\FormStateInterface $form_state
202 * The current state of the form.
204 public function submitOverviewForm(array &$form, FormStateInterface $form_state) {
205 $form_state->set('step', 'credentials');
206 $form_state->setRebuild();
210 * Builds the database credential form and adds file location information.
212 * This is largely borrowed from \Drupal\Core\Installer\Form\SiteSettingsForm.
215 * An associative array containing the structure of the form.
216 * @param \Drupal\Core\Form\FormStateInterface $form_state
217 * The current state of the form.
220 * The form structure.
222 * @todo Private files directory not yet implemented, depends on
223 * https://www.drupal.org/node/2547125.
225 public function buildCredentialForm(array $form, FormStateInterface $form_state) {
226 $form['#title'] = $this->t('Drupal Upgrade');
228 $drivers = $this->getDatabaseTypes();
229 $drivers_keys = array_keys($drivers);
230 // @todo https://www.drupal.org/node/2678510 Because this is a multi-step
231 // form, the form is not rebuilt during submission. Ideally we would get
232 // the chosen driver from form input, if available, in order to use
233 // #limit_validation_errors in the same way
234 // \Drupal\Core\Installer\Form\SiteSettingsForm does.
235 $default_driver = current($drivers_keys);
237 $default_options = [];
241 '#default_value' => 7,
242 '#title' => $this->t('Drupal version of the source site'),
243 '#options' => [6 => $this->t('Drupal 6'), 7 => $this->t('Drupal 7')],
247 $form['database'] = [
248 '#type' => 'details',
249 '#title' => $this->t('Source database'),
250 '#description' => $this->t('Provide credentials for the database of the Drupal site you want to upgrade.'),
254 $form['database']['driver'] = [
256 '#title' => $this->t('Database type'),
258 '#default_value' => $default_driver,
260 if (count($drivers) == 1) {
261 $form['database']['driver']['#disabled'] = TRUE;
264 // Add driver-specific configuration options.
265 foreach ($drivers as $key => $driver) {
266 $form['database']['driver']['#options'][$key] = $driver->name();
268 $form['database']['settings'][$key] = $driver->getFormOptions($default_options);
269 // @todo https://www.drupal.org/node/2678510 Using
270 // #limit_validation_errors in the submit does not work so it is not
271 // possible to require the database and username for mysql and pgsql.
272 // This is because this is a multi-step form.
273 $form['database']['settings'][$key]['database']['#required'] = FALSE;
274 $form['database']['settings'][$key]['username']['#required'] = FALSE;
275 $form['database']['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . $this->t('@driver_name settings', ['@driver_name' => $driver->name()]) . '</h2>';
276 $form['database']['settings'][$key]['#type'] = 'container';
277 $form['database']['settings'][$key]['#tree'] = TRUE;
278 $form['database']['settings'][$key]['advanced_options']['#parents'] = [$key];
279 $form['database']['settings'][$key]['#states'] = [
281 ':input[name=driver]' => ['value' => $key],
285 // Move the host fields out of advanced settings.
286 if (isset($form['database']['settings'][$key]['advanced_options']['host'])) {
287 $form['database']['settings'][$key]['host'] = $form['database']['settings'][$key]['advanced_options']['host'];
288 $form['database']['settings'][$key]['host']['#title'] = 'Database host';
289 $form['database']['settings'][$key]['host']['#weight'] = -1;
290 unset($form['database']['settings'][$key]['database']['#default_value']);
291 unset($form['database']['settings'][$key]['advanced_options']['host']);
296 '#type' => 'details',
297 '#title' => $this->t('Source files'),
300 $form['source']['d6_source_base_path'] = [
301 '#type' => 'textfield',
302 '#title' => $this->t('Files directory'),
303 '#description' => $this->t('To import files from your current Drupal site, enter a local file directory containing your site (e.g. /var/www/docroot), or your site address (for example http://example.com).'),
306 ':input[name="version"]' => ['value' => 6],
311 $form['source']['source_base_path'] = [
312 '#type' => 'textfield',
313 '#title' => $this->t('Public files directory'),
314 '#description' => $this->t('To import public files from your current Drupal site, enter a local file directory containing your site (e.g. /var/www/docroot), or your site address (for example http://example.com).'),
317 ':input[name="version"]' => ['value' => 7],
322 $form['source']['source_private_file_path'] = [
323 '#type' => 'textfield',
324 '#title' => $this->t('Private file directory'),
325 '#default_value' => '',
326 '#description' => $this->t('To import private files from your current Drupal site, enter a local file directory containing your site (e.g. /var/www/docroot).'),
329 ':input[name="version"]' => ['value' => 7],
334 $form['actions'] = ['#type' => 'actions'];
335 $form['actions']['save'] = [
337 '#value' => $this->t('Review upgrade'),
338 '#button_type' => 'primary',
339 '#validate' => ['::validateCredentialForm'],
340 '#submit' => ['::submitCredentialForm'],
346 * Validation handler for the credentials form.
349 * An associative array containing the structure of the form.
350 * @param \Drupal\Core\Form\FormStateInterface $form_state
351 * The current state of the form.
353 public function validateCredentialForm(array &$form, FormStateInterface $form_state) {
355 // Retrieve the database driver from the form, use reflection to get the
356 // namespace, and then construct a valid database array the same as in
358 $driver = $form_state->getValue('driver');
359 $drivers = $this->getDatabaseTypes();
360 $reflection = new \ReflectionClass($drivers[$driver]);
361 $install_namespace = $reflection->getNamespaceName();
363 $database = $form_state->getValue($driver);
364 // Cut the trailing \Install from namespace.
365 $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
366 $database['driver'] = $driver;
368 // Validate the driver settings and just end here if we have any issues.
369 if ($errors = $drivers[$driver]->validateDatabaseSettings($database)) {
370 foreach ($errors as $name => $message) {
371 $form_state->setErrorByName($name, $message);
377 $connection = $this->getConnection($database);
378 $version = $this->getLegacyDrupalVersion($connection);
380 $form_state->setErrorByName($database['driver'] . '][0', $this->t('Source database does not contain a recognizable Drupal version.'));
382 elseif ($version != $form_state->getValue('version')) {
383 $form_state->setErrorByName($database['driver'] . '][0', $this->t('Source database is Drupal version @version but version @selected was selected.', [
384 '@version' => $version,
385 '@selected' => $form_state->getValue('version'),
389 $this->createDatabaseStateSettings($database, $version);
390 $migrations = $this->getMigrations('migrate_drupal_' . $version, $version);
392 // Get the system data from source database.
393 $system_data = $this->getSystemData($connection);
395 // Convert the migration object into array
396 // so that it can be stored in form storage.
397 $migration_array = [];
398 foreach ($migrations as $migration) {
399 $migration_array[$migration->id()] = $migration->label();
402 // Store the retrieved migration IDs in form storage.
403 $form_state->set('version', $version);
404 $form_state->set('migrations', $migration_array);
406 $form_state->set('source_base_path', $form_state->getValue('d6_source_base_path'));
409 $form_state->set('source_base_path', $form_state->getValue('source_base_path'));
411 $form_state->set('source_private_file_path', $form_state->getValue('source_private_file_path'));
412 // Store the retrived system data in form storage.
413 $form_state->set('system_data', $system_data);
416 catch (\Exception $e) {
418 '#title' => $this->t('Resolve the issue below to continue the upgrade.'),
419 '#theme' => 'item_list',
420 '#items' => [$e->getMessage()],
422 $form_state->setErrorByName($database['driver'] . '][0', $this->renderer->renderPlain($error_message));
427 * Submission handler for the credentials form.
430 * An associative array containing the structure of the form.
431 * @param \Drupal\Core\Form\FormStateInterface $form_state
432 * The current state of the form.
434 public function submitCredentialForm(array &$form, FormStateInterface $form_state) {
435 // Indicate the next step is confirmation.
436 $form_state->set('step', 'confirm');
437 $form_state->setRebuild();
441 * Confirmation form for missing migrations, etc.
444 * An associative array containing the structure of the form.
445 * @param \Drupal\Core\Form\FormStateInterface $form_state
446 * The current state of the form.
449 * The form structure.
451 public function buildConfirmForm(array $form, FormStateInterface $form_state) {
452 $form = parent::buildForm($form, $form_state);
453 $form['actions']['submit']['#submit'] = ['::submitConfirmForm'];
455 $form['actions']['submit']['#value'] = $this->t('Perform upgrade');
457 $version = $form_state->get('version');
458 $migrations = $this->getMigrations('migrate_drupal_' . $version, $version);
461 foreach ($migrations as $migration) {
462 $migration_id = $migration->getPluginId();
463 $source_module = $migration->getSourcePlugin()->getSourceModule();
464 if (!$source_module) {
465 drupal_set_message($this->t('Source module not found for @migration_id.', ['@migration_id' => $migration_id]), 'error');
467 $destination_module = $migration->getDestinationPlugin()->getDestinationModule();
468 if (!$destination_module) {
469 drupal_set_message($this->t('Destination module not found for @migration_id.', ['@migration_id' => $migration_id]), 'error');
472 if ($source_module && $destination_module) {
473 $table_data[$source_module][$destination_module][$migration_id] = $migration->label();
476 // Sort the table by source module names and within that destination
479 foreach ($table_data as $source_module => $destination_module_info) {
480 ksort($table_data[$source_module]);
483 // Fetch the system data at the first opportunity.
484 $system_data = $form_state->get('system_data');
485 $unmigrated_source_modules = array_diff_key($system_data['module'], $table_data);
487 // Missing migrations.
488 $missing_module_list = [
489 '#type' => 'details',
492 '#type' => 'html_tag',
494 '#value' => $this->t('Missing upgrade paths'),
495 '#attributes' => ['id' => ['warning']],
497 '#description' => $this->t('The following items will not be upgraded. For more information see <a href=":migrate">Upgrading from Drupal 6 or 7 to Drupal 8</a>.', [':migrate' => 'https://www.drupal.org/upgrade/migrate']),
500 $missing_module_list['module_list'] = [
503 $this->t('Source module: Drupal @version', ['@version' => $version]),
504 $this->t('Upgrade module: Drupal 8'),
508 ksort($unmigrated_source_modules);
509 foreach ($unmigrated_source_modules as $source_module => $module_data) {
510 if ($module_data['status']) {
512 $missing_module_list['module_list'][$source_module] = [
514 '#type' => 'html_tag',
516 '#value' => $source_module,
519 'upgrade-analysis-report__status-icon',
520 'upgrade-analysis-report__status-icon--warning',
524 'destination_module' => ['#plain_text' => 'Missing'],
528 // Available migrations.
529 $available_module_list = [
530 '#type' => 'details',
532 '#type' => 'html_tag',
534 '#value' => $this->t('Available upgrade paths'),
535 '#attributes' => ['id' => ['checked']],
540 $available_module_list['module_list'] = [
543 $this->t('Source module: Drupal @version', ['@version' => $version]),
544 $this->t('Upgrade module: Drupal 8'),
548 $available_count = 0;
549 foreach ($table_data as $source_module => $destination_module_info) {
551 $destination_details = [];
552 foreach ($destination_module_info as $destination_module => $migration_ids) {
553 $destination_details[$destination_module] = [
555 '#plain_text' => $destination_module,
558 $available_module_list['module_list'][$source_module] = [
560 '#type' => 'html_tag',
562 '#value' => $source_module,
565 'upgrade-analysis-report__status-icon',
566 'upgrade-analysis-report__status-icon--checked',
570 'destination_module' => $destination_details,
577 if ($missing_count) {
579 '#theme' => 'status_report_counter',
580 '#amount' => $missing_count,
581 '#text' => $this->formatPlural($missing_count, 'Missing upgrade path', 'Missing upgrade paths'),
582 '#severity' => 'warning',
585 $general_info[] = $missing_module_list;
587 if ($available_count) {
589 '#theme' => 'status_report_counter',
590 '#amount' => $available_count,
591 '#text' => $this->formatPlural($available_count, 'Available upgrade path', 'Available upgrade paths'),
592 '#severity' => 'checked',
595 $general_info[] = $available_module_list;
598 $form['status_report_page'] = [
599 '#theme' => 'status_report_page',
600 '#counters' => $counters,
601 '#general_info' => $general_info,
604 $form['#attached']['library'][] = 'migrate_drupal_ui/base';
610 * Submission handler for the confirmation form.
613 * An associative array containing the structure of the form.
614 * @param \Drupal\Core\Form\FormStateInterface $form_state
615 * The current state of the form.
617 public function submitConfirmForm(array &$form, FormStateInterface $form_state) {
618 $storage = $form_state->getStorage();
620 $migrations = $storage['migrations'];
621 $config['source_base_path'] = $storage['source_base_path'];
623 'title' => $this->t('Running upgrade'),
624 'progress_message' => '',
627 [MigrateUpgradeImportBatch::class, 'run'],
628 [array_keys($migrations), $config],
632 MigrateUpgradeImportBatch::class, 'finished',
636 $form_state->setRedirect('<front>');
637 $this->state->set('migrate_drupal_ui.performed', REQUEST_TIME);
641 * Returns all supported database driver installer objects.
643 * @return \Drupal\Core\Database\Install\Tasks[]
644 * An array of available database driver installer objects.
646 protected function getDatabaseTypes() {
647 // Make sure the install API is available.
648 include_once DRUPAL_ROOT . '/core/includes/install.inc';
649 return drupal_get_database_types();
655 public function getQuestion() {
656 return $this->t('Upgrade analysis report');
662 public function getCancelUrl() {
663 return new Url('migrate_drupal_ui.upgrade');
669 public function getDescription() {
670 // The description is added by the buildConfirmForm() method.
671 // @see \Drupal\migrate_drupal_ui\Form\MigrateUpgradeForm::buildConfirmForm()
678 public function getConfirmText() {
679 return $this->t('Perform upgrade');