Patched to Drupal 8.4.8 level. See https://www.drupal.org/sa-core-2018-004 and patch...
[yaffs-website] / web / core / modules / migrate_drupal_ui / src / Form / MigrateUpgradeForm.php
1 <?php
2
3 namespace Drupal\migrate_drupal_ui\Form;
4
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;
10 use Drupal\Core\Url;
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;
15
16 /**
17  * Defines a multi-step form for performing direct site upgrades.
18  */
19 class MigrateUpgradeForm extends ConfirmFormBase {
20
21   use MigrationConfigurationTrait;
22
23   /**
24    * The state service.
25    *
26    * @var \Drupal\Core\State\StateInterface
27    */
28   protected $state;
29
30   /**
31    * The date formatter service.
32    *
33    * @var \Drupal\Core\Datetime\DateFormatterInterface
34    */
35   protected $dateFormatter;
36
37   /**
38    * The renderer service.
39    *
40    * @var \Drupal\Core\Render\RendererInterface
41    */
42   protected $renderer;
43
44   /**
45    * The migration plugin manager.
46    *
47    * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
48    */
49   protected $pluginManager;
50
51   /**
52    * Constructs the MigrateUpgradeForm.
53    *
54    * @param \Drupal\Core\State\StateInterface $state
55    *   The state service.
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.
62    */
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;
68   }
69
70   /**
71    * {@inheritdoc}
72    */
73   public static function create(ContainerInterface $container) {
74     return new static(
75       $container->get('state'),
76       $container->get('date.formatter'),
77       $container->get('renderer'),
78       $container->get('plugin.manager.migration')
79     );
80   }
81
82   /**
83    * {@inheritdoc}
84    */
85   public function getFormId() {
86     return 'migrate_drupal_ui_form';
87   }
88
89   /**
90    * {@inheritdoc}
91    */
92   public function buildForm(array $form, FormStateInterface $form_state) {
93     $step = $form_state->get('step') ?: 'overview';
94     switch ($step) {
95       case 'overview':
96         return $this->buildOverviewForm($form, $form_state);
97
98       case 'credentials':
99         return $this->buildCredentialForm($form, $form_state);
100
101       case 'confirm':
102         return $this->buildConfirmForm($form, $form_state);
103
104       default:
105         drupal_set_message($this->t('Unrecognized form step @step', ['@step' => $step]), 'error');
106         return [];
107     }
108   }
109
110   /**
111    * {@inheritdoc}
112    */
113   public function submitForm(array &$form, FormStateInterface $form_state) {
114     // This method is intentionally empty, see the specific submit methods for
115     // each form step.
116   }
117
118   /**
119    * Builds the form presenting an overview of the migration process.
120    *
121    * @param array $form
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.
125    *
126    * @return array
127    *   The form structure.
128    */
129   public function buildOverviewForm(array $form, FormStateInterface $form_state) {
130     $form['#title'] = $this->t('Upgrade');
131
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'] = [
137         '#type' => '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)]),
140       ];
141       return $form;
142     }
143     else {
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',
147         ]),
148       ];
149
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.');
152
153       $form['legend'] = [
154         '#theme' => 'item_list',
155         '#title' => $this->t('Definitions'),
156         '#list_type' => 'ul',
157         '#items' => $legend,
158       ];
159
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',
166       ]);
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(),
169       ]);
170
171       $form['info'] = [
172         '#theme' => 'item_list',
173         '#title' => $this->t('Steps to prepare for the upgrade'),
174         '#list_type' => 'ol',
175         '#items' => $info,
176       ];
177
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.'),
180       ];
181
182       $validate = [];
183     }
184
185     $form['actions'] = ['#type' => 'actions'];
186     $form['actions']['save'] = [
187       '#type' => 'submit',
188       '#value' => $this->t('Continue'),
189       '#button_type' => 'primary',
190       '#validate' => $validate,
191       '#submit' => ['::submitOverviewForm'],
192     ];
193     return $form;
194   }
195
196   /**
197    * Form submission handler for the overview form.
198    *
199    * @param array $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.
203    */
204   public function submitOverviewForm(array &$form, FormStateInterface $form_state) {
205     $form_state->set('step', 'credentials');
206     $form_state->setRebuild();
207   }
208
209   /**
210    * Builds the database credential form and adds file location information.
211    *
212    * This is largely borrowed from \Drupal\Core\Installer\Form\SiteSettingsForm.
213    *
214    * @param array $form
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.
218    *
219    * @return array
220    *   The form structure.
221    *
222    * @todo Private files directory not yet implemented, depends on
223    *   https://www.drupal.org/node/2547125.
224    */
225   public function buildCredentialForm(array $form, FormStateInterface $form_state) {
226     $form['#title'] = $this->t('Drupal Upgrade');
227
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);
236
237     $default_options = [];
238
239     $form['version'] = [
240       '#type' => 'radios',
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')],
244       '#required' => TRUE,
245     ];
246
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.'),
251       '#open' => TRUE,
252     ];
253
254     $form['database']['driver'] = [
255       '#type' => 'radios',
256       '#title' => $this->t('Database type'),
257       '#required' => TRUE,
258       '#default_value' => $default_driver,
259     ];
260     if (count($drivers) == 1) {
261       $form['database']['driver']['#disabled'] = TRUE;
262     }
263
264     // Add driver-specific configuration options.
265     foreach ($drivers as $key => $driver) {
266       $form['database']['driver']['#options'][$key] = $driver->name();
267
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'] = [
280         'visible' => [
281           ':input[name=driver]' => ['value' => $key],
282         ],
283       ];
284
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']);
292       }
293     }
294
295     $form['source'] = [
296       '#type' => 'details',
297       '#title' => $this->t('Source files'),
298       '#open' => TRUE,
299     ];
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).'),
304       '#states' => [
305         'visible' => [
306           ':input[name="version"]' => ['value' => 6],
307         ],
308       ],
309     ];
310
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).'),
315       '#states' => [
316         'visible' => [
317           ':input[name="version"]' => ['value' => 7],
318         ],
319       ],
320     ];
321
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).'),
327       '#states' => [
328         'visible' => [
329           ':input[name="version"]' => ['value' => 7],
330         ],
331       ],
332     ];
333
334     $form['actions'] = ['#type' => 'actions'];
335     $form['actions']['save'] = [
336       '#type' => 'submit',
337       '#value' => $this->t('Review upgrade'),
338       '#button_type' => 'primary',
339       '#validate' => ['::validateCredentialForm'],
340       '#submit' => ['::submitCredentialForm'],
341     ];
342     return $form;
343   }
344
345   /**
346    * Validation handler for the credentials form.
347    *
348    * @param array $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.
352    */
353   public function validateCredentialForm(array &$form, FormStateInterface $form_state) {
354
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
357     // settings.php.
358     $driver = $form_state->getValue('driver');
359     $drivers = $this->getDatabaseTypes();
360     $reflection = new \ReflectionClass($drivers[$driver]);
361     $install_namespace = $reflection->getNamespaceName();
362
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;
367
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);
372       }
373       return;
374     }
375
376     try {
377       $connection = $this->getConnection($database);
378       $version = $this->getLegacyDrupalVersion($connection);
379       if (!$version) {
380         $form_state->setErrorByName($database['driver'] . '][0', $this->t('Source database does not contain a recognizable Drupal version.'));
381       }
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'),
386         ]));
387       }
388       else {
389         $this->createDatabaseStateSettings($database, $version);
390         $migrations = $this->getMigrations('migrate_drupal_' . $version, $version);
391
392         // Get the system data from source database.
393         $system_data = $this->getSystemData($connection);
394
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();
400         }
401
402         // Store the retrieved migration IDs in form storage.
403         $form_state->set('version', $version);
404         $form_state->set('migrations', $migration_array);
405         if ($version == 6) {
406           $form_state->set('source_base_path', $form_state->getValue('d6_source_base_path'));
407         }
408         else {
409           $form_state->set('source_base_path', $form_state->getValue('source_base_path'));
410         }
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);
414       }
415     }
416     catch (\Exception $e) {
417       $error_message = [
418         '#title' => $this->t('Resolve the issue below to continue the upgrade.'),
419         '#theme' => 'item_list',
420         '#items' => [$e->getMessage()],
421       ];
422       $form_state->setErrorByName($database['driver'] . '][0', $this->renderer->renderPlain($error_message));
423     }
424   }
425
426   /**
427    * Submission handler for the credentials form.
428    *
429    * @param array $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.
433    */
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();
438   }
439
440   /**
441    * Confirmation form for missing migrations, etc.
442    *
443    * @param array $form
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.
447    *
448    * @return array
449    *   The form structure.
450    */
451   public function buildConfirmForm(array $form, FormStateInterface $form_state) {
452     $form = parent::buildForm($form, $form_state);
453     $form['actions']['submit']['#submit'] = ['::submitConfirmForm'];
454
455     $form['actions']['submit']['#value'] = $this->t('Perform upgrade');
456
457     $version = $form_state->get('version');
458     $migrations = $this->getMigrations('migrate_drupal_' . $version, $version);
459
460     $table_data = [];
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');
466       }
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');
470       }
471
472       if ($source_module && $destination_module) {
473         $table_data[$source_module][$destination_module][$migration_id] = $migration->label();
474       }
475     }
476     // Sort the table by source module names and within that destination
477     // module names.
478     ksort($table_data);
479     foreach ($table_data as $source_module => $destination_module_info) {
480       ksort($table_data[$source_module]);
481     }
482
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);
486
487     // Missing migrations.
488     $missing_module_list = [
489       '#type' => 'details',
490       '#open' => TRUE,
491       '#title' => [
492         '#type' => 'html_tag',
493         '#tag' => 'span',
494         '#value' => $this->t('Missing upgrade paths'),
495         '#attributes' => ['id' => ['warning']],
496       ],
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']),
498       '#weight' => 2,
499     ];
500     $missing_module_list['module_list'] = [
501       '#type' => 'table',
502       '#header' => [
503         $this->t('Source module: Drupal @version', ['@version' => $version]),
504         $this->t('Upgrade module: Drupal 8'),
505       ],
506     ];
507     $missing_count = 0;
508     ksort($unmigrated_source_modules);
509     foreach ($unmigrated_source_modules as $source_module => $module_data) {
510       if ($module_data['status']) {
511         $missing_count++;
512         $missing_module_list['module_list'][$source_module] = [
513           'source_module' => [
514             '#type' => 'html_tag',
515             '#tag' => 'span',
516             '#value' => $source_module,
517             '#attributes' => [
518               'class' => [
519                 'upgrade-analysis-report__status-icon',
520                 'upgrade-analysis-report__status-icon--warning',
521               ],
522             ],
523           ],
524           'destination_module' => ['#plain_text' => 'Missing'],
525         ];
526       }
527     }
528     // Available migrations.
529     $available_module_list = [
530       '#type' => 'details',
531       '#title' => [
532         '#type' => 'html_tag',
533         '#tag' => 'span',
534         '#value' => $this->t('Available upgrade paths'),
535         '#attributes' => ['id' => ['checked']],
536       ],
537       '#weight' => 3,
538     ];
539
540     $available_module_list['module_list'] = [
541       '#type' => 'table',
542       '#header' => [
543         $this->t('Source module: Drupal @version', ['@version' => $version]),
544         $this->t('Upgrade module: Drupal 8'),
545       ],
546     ];
547
548     $available_count = 0;
549     foreach ($table_data as $source_module => $destination_module_info) {
550       $available_count++;
551       $destination_details = [];
552       foreach ($destination_module_info as $destination_module => $migration_ids) {
553         $destination_details[$destination_module] = [
554           '#type' => 'item',
555           '#plain_text' => $destination_module,
556         ];
557       }
558       $available_module_list['module_list'][$source_module] = [
559         'source_module' => [
560           '#type' => 'html_tag',
561           '#tag' => 'span',
562           '#value' => $source_module,
563           '#attributes' => [
564             'class' => [
565               'upgrade-analysis-report__status-icon',
566               'upgrade-analysis-report__status-icon--checked',
567             ],
568           ],
569         ],
570         'destination_module' => $destination_details,
571       ];
572     }
573
574     $counters = [];
575     $general_info = [];
576
577     if ($missing_count) {
578       $counters[] = [
579         '#theme' => 'status_report_counter',
580         '#amount' => $missing_count,
581         '#text' => $this->formatPlural($missing_count, 'Missing upgrade path', 'Missing upgrade paths'),
582         '#severity' => 'warning',
583         '#weight' => 0,
584       ];
585       $general_info[] = $missing_module_list;
586     }
587     if ($available_count) {
588       $counters[] = [
589         '#theme' => 'status_report_counter',
590         '#amount' => $available_count,
591         '#text' => $this->formatPlural($available_count, 'Available upgrade path', 'Available upgrade paths'),
592         '#severity' => 'checked',
593         '#weight' => 1,
594       ];
595       $general_info[] = $available_module_list;
596     }
597
598     $form['status_report_page'] = [
599       '#theme' => 'status_report_page',
600       '#counters' => $counters,
601       '#general_info' => $general_info,
602     ];
603
604     $form['#attached']['library'][] = 'migrate_drupal_ui/base';
605
606     return $form;
607   }
608
609   /**
610    * Submission handler for the confirmation form.
611    *
612    * @param array $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.
616    */
617   public function submitConfirmForm(array &$form, FormStateInterface $form_state) {
618     $storage = $form_state->getStorage();
619
620     $migrations = $storage['migrations'];
621     $config['source_base_path'] = $storage['source_base_path'];
622     $batch = [
623       'title' => $this->t('Running upgrade'),
624       'progress_message' => '',
625       'operations' => [
626         [
627           [MigrateUpgradeImportBatch::class, 'run'],
628           [array_keys($migrations), $config],
629         ],
630       ],
631       'finished' => [
632         MigrateUpgradeImportBatch::class, 'finished',
633       ],
634     ];
635     batch_set($batch);
636     $form_state->setRedirect('<front>');
637     $this->state->set('migrate_drupal_ui.performed', REQUEST_TIME);
638   }
639
640   /**
641    * Returns all supported database driver installer objects.
642    *
643    * @return \Drupal\Core\Database\Install\Tasks[]
644    *   An array of available database driver installer objects.
645    */
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();
650   }
651
652   /**
653    * {@inheritdoc}
654    */
655   public function getQuestion() {
656     return $this->t('Upgrade analysis report');
657   }
658
659   /**
660    * {@inheritdoc}
661    */
662   public function getCancelUrl() {
663     return new Url('migrate_drupal_ui.upgrade');
664   }
665
666   /**
667    * {@inheritdoc}
668    */
669   public function getDescription() {
670     // The description is added by the buildConfirmForm() method.
671     // @see \Drupal\migrate_drupal_ui\Form\MigrateUpgradeForm::buildConfirmForm()
672     return;
673   }
674
675   /**
676    * {@inheritdoc}
677    */
678   public function getConfirmText() {
679     return $this->t('Perform upgrade');
680   }
681
682 }