first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
/**
* @file
* Styles for the upgrade analysis report tables.
*/
.upgrade-analysis-report__status-icon::before {
display: inline-block;
width: 32px;
height: 14px;
content: "";
background-repeat: no-repeat;
background-position: left center;
background-size: 16px;
}
.upgrade-analysis-report__status-icon--warning::before {
background-image: url(../../../../misc/icons/e29700/warning.svg);
}
.upgrade-analysis-report__status-icon--checked::before {
background-image: url(../../../../misc/icons/73b355/check.svg);
}
.upgrade-analysis-report__status-icon--error::before {
background-image: url(../../../../misc/icons/e32700/error.svg);
}

View File

@@ -0,0 +1,23 @@
---
label: 'Migrating data for an upgrade using the user interface'
related:
- migrate.overview
---
{% set migrate_drupal_upgrade_link_text %}{% trans %}Upgrade{% endtrans %}{% endset %}
{% set migrate_drupal_upgrade_link = render_var(help_route_link(migrate_drupal_upgrade_link_text, 'migrate_drupal_ui.upgrade')) %}
{% set migrate_overview_topic = render_var(help_topic_link('migrate.overview')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Migrate data into a new, empty site, as part of an upgrade from an older version of Drupal. See {{ migrate_overview_topic }} for an overview of migrating and upgrading.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em>{{ migrate_drupal_upgrade_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Read the introduction and follow the <em>Preparation steps</em> on the page. Then click <em>Continue</em>.{% endtrans %}</li>
<li>{% trans %}Select the Drupal version of the source site. Also enter the database credentials and public and private files directories (private file directory is not available when migrating from Drupal 6). Click <em>Review upgrade</em>.{% endtrans %}</li>
<li>{% trans %}If the next page you see contains a message about conflicting content, that means that the site where you are running the upgrade is not empty. If you continue, you will lose the data in the site. If that is acceptable, click the button to continue; if not, start these steps over in a new, clean Drupal site.{% endtrans %}</li>
<li>{% trans %}On the <em>What will be upgraded?</em> page, review the list of modules whose data will not be upgraded. If that list is not empty and you want to migrate the data from those modules, you will need to download contributed modules and/or install core or contributed modules, and start these steps over.{% endtrans %}</li>
<li>{% trans %}When the list of modules that will and will not be upgraded meets your expectations, click <em>Perform upgrade</em> and wait for the upgrade to complete. You will see a message about the number of upgrade tasks that were successful or failed, and you can review the upgrade message log by clicking the link on the results page.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional Resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/upgrading-drupal/upgrading-from-drupal-6-or-7-to-drupal-8-and-newer">Upgrading from Drupal 6 or 7 to Drupal 8 (and newer)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,15 @@
name: 'Migrate Drupal UI'
type: module
description: 'Provides a user interface for migrating from older Drupal versions.'
package: Migration
# version: VERSION
configure: migrate_drupal_ui.upgrade
dependencies:
- drupal:migrate
- drupal:migrate_drupal
- drupal:dblog
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Install, update, and uninstall functions for the migrate_drupal_ui module.
*/
use Drupal\Core\Url;
/**
* Implements hook_install().
*/
function migrate_drupal_ui_install() {
$url = Url::fromRoute('migrate_drupal_ui.upgrade')->toString();
\Drupal::messenger()->addStatus(t('The Migrate Drupal UI module has been enabled. Proceed to the <a href=":url">upgrade form</a>.', [':url' => $url]));
}

View File

@@ -0,0 +1,5 @@
base:
version: VERSION
css:
component:
css/components/upgrade-analysis-report-tables.css: {}

View File

@@ -0,0 +1,12 @@
migrate_drupal_ui.upgrade:
title: 'Upgrade'
parent: system.admin_config_development
description: 'Upgrade content and configuration from either a Drupal 6 or a Drupal 7 site.'
route_name: migrate_drupal_ui.upgrade
migrate_drupal_ui.log:
title: 'Upgrade log'
parent: system.admin_reports
description: 'View the upgrade log.'
route_name: migrate_drupal_ui.log
weight: 0

View File

@@ -0,0 +1,41 @@
<?php
/**
* @file
* Alert administrators before starting the import process.
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Implements hook_help().
*/
function migrate_drupal_ui_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.migrate_drupal_ui':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Migrate Drupal UI module provides a simple user interface to perform an upgrade from an earlier version of Drupal. For more information, see the <a href=":migrate">online documentation for the Migrate Drupal UI module</a>.',
[':migrate' => 'https://www.drupal.org/upgrade/migrate']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Preparing the site') . '</dt>';
$output .= '<dd>' . t('You need to install all modules on this site that are installed on the previous site. For example, if you have used the Book module on the previous site then you must install the Book module on this site for that data to be available on this site.') . '</dd>';
$output .= '<dt>' . t('Performing the upgrade') . '</dt>';
$output .= '<dd>' . t('On the <a href=":upgrade">Upgrade</a> page, you are guided through performing the upgrade in several steps.',
[':upgrade' => Url::fromRoute('migrate_drupal_ui.upgrade')->toString()]) . '</dd>';
$output .= '<dd><ol><li>' . t('If an upgrade has been performed on this site, you will be informed.') . '</li>';
$output .= '<li>' . t('You need to enter the database credentials of the Drupal site that you want to upgrade. You can also include its files directory in the upgrade. For example local files, /var/www/docroot, or remote files http://www.example.com.') . '</li>';
$output .= '<li>' . t('If there is existing content on the site that may be overwritten by this upgrade, you will be informed.') . '</li>';
$output .= '<li>' . t('The next page provides an overview of the modules that will be upgraded and those that will not be upgraded, before you proceed to perform the upgrade.') . '</li>';
$output .= '<li>' . t('Finally, a message is displayed about the number of upgrade tasks that were successful or failed.') . '</li></ol></dd>';
$output .= '<dt>' . t('Reviewing the upgrade log') . '</dt>';
$output .= '<dd>' . t('You can review a <a href=":log">log of upgrade messages</a> by clicking the link in the message provided after the upgrade or by filtering the messages for the type <em>migrate_drupal_ui</em> on the <a href=":messages">Recent log messages</a> page.',
[':log' => Url::fromRoute('migrate_drupal_ui.log')->toString(), ':messages' => Url::fromRoute('dblog.overview')->toString()]) . '</dd>';
$output .= '<dt>' . t('Rolling back an upgrade') . '</dt>';
$output .= '<dd>' . t('Rolling back an upgrade is not yet supported through the user interface.') . '</dd>';
$output .= '</dl>';
return $output;
}
}

View File

@@ -0,0 +1,55 @@
# cspell:ignore idconflict
migrate_drupal_ui.upgrade:
path: '/upgrade'
defaults:
_form: '\Drupal\migrate_drupal_ui\Form\OverviewForm'
_title: 'Upgrade'
requirements:
_custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
options:
_admin_route: TRUE
migrate_drupal_ui.upgrade_incremental:
path: '/upgrade/incremental'
defaults:
_form: '\Drupal\migrate_drupal_ui\Form\IncrementalForm'
_title: 'Upgrade'
requirements:
_custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
options:
_admin_route: TRUE
migrate_drupal_ui.upgrade_credential:
path: '/upgrade/credentials'
defaults:
_form: '\Drupal\migrate_drupal_ui\Form\CredentialForm'
_title: 'Upgrade'
requirements:
_custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
options:
_admin_route: TRUE
migrate_drupal_ui.upgrade_idconflict:
path: '/upgrade/idconflict'
defaults:
_form: '\Drupal\migrate_drupal_ui\Form\IdConflictForm'
_title: 'Upgrade'
requirements:
_custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
options:
_admin_route: TRUE
migrate_drupal_ui.upgrade_review:
path: '/upgrade/review'
defaults:
_form: '\Drupal\migrate_drupal_ui\Form\ReviewForm'
_title: 'Upgrade'
requirements:
_custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
options:
_admin_route: TRUE
migrate_drupal_ui.log:
path: '/admin/reports/upgrade'
defaults:
_controller: '\Drupal\migrate_drupal_ui\Controller\MigrateController::showLog'
requirements:
_custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
options:
_admin_route: TRUE

View File

@@ -0,0 +1,5 @@
services:
migrate_drupal_ui.route_subscriber:
class: Drupal\migrate_drupal_ui\Routing\MigrateDrupalUiRouteSubscriber
tags:
- { name: event_subscriber }

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\migrate_drupal_ui\Batch;
use Drupal\migrate\MigrateMessageInterface;
/**
* Allows capturing messages rather than displaying them directly.
*/
class MigrateMessageCapture implements MigrateMessageInterface {
/**
* Array of recorded messages.
*
* @var array
*/
protected $messages = [];
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status') {
$this->messages[] = $message;
}
/**
* Clears out any captured messages.
*/
public function clear() {
$this->messages = [];
}
/**
* Returns any captured messages.
*
* @return array
* The captured messages.
*/
public function getMessages() {
return $this->messages;
}
}

View File

@@ -0,0 +1,377 @@
<?php
namespace Drupal\migrate_drupal_ui\Batch;
use Drupal\Core\Link;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate_drupal\Plugin\MigrationWithFollowUpInterface;
// cspell:ignore idmap
/**
* Runs a single migration batch.
*/
class MigrateUpgradeImportBatch {
/**
* Maximum number of previous messages to display.
*/
const MESSAGE_LENGTH = 20;
/**
* The processed items for one batch of a given migration.
*
* @var int
*/
protected static $numProcessed = 0;
/**
* Ensure we only add the listeners once per request.
*
* @var bool
*/
protected static $listenersAdded = FALSE;
/**
* The maximum length in seconds to allow processing in a request.
*
* @see self::run()
*
* @var int
*/
protected static $maxExecTime;
/**
* MigrateMessage instance to capture messages during the migration process.
*
* @var \Drupal\migrate_drupal_ui\Batch\MigrateMessageCapture
*/
protected static $messages;
/**
* The follow-up migrations.
*
* @var \Drupal\migrate\Plugin\MigrationInterface[]
*/
protected static $followUpMigrations;
/**
* Runs a single migrate batch import.
*
* @param int[] $initial_ids
* The full set of migration IDs to import.
* @param array $config
* An array of additional configuration from the form.
* @param array $context
* The batch context.
*/
public static function run($initial_ids, $config, &$context) {
if (!static::$listenersAdded) {
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']);
$event_dispatcher->addListener(MigrateEvents::POST_IMPORT, [static::class, 'onPostImport']);
$event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']);
$event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']);
static::$maxExecTime = ini_get('max_execution_time');
if (static::$maxExecTime <= 0) {
static::$maxExecTime = 60;
}
// Set an arbitrary threshold of 3 seconds (e.g., if max_execution_time is
// 45 seconds, we will quit at 42 seconds so a slow item or cleanup
// overhead don't put us over 45).
static::$maxExecTime -= 3;
static::$listenersAdded = TRUE;
}
if (!isset($context['sandbox']['migration_ids'])) {
$context['sandbox']['max'] = count($initial_ids);
$context['sandbox']['current'] = 1;
// Total number processed for this migration.
$context['sandbox']['num_processed'] = 0;
// migration_ids will be the list of IDs remaining to run.
$context['sandbox']['migration_ids'] = $initial_ids;
$context['sandbox']['messages'] = [];
$context['results']['failures'] = 0;
$context['results']['successes'] = 0;
}
// Number processed in this batch.
static::$numProcessed = 0;
$migration_id = reset($context['sandbox']['migration_ids']);
$definition = \Drupal::service('plugin.manager.migration')->getDefinition($migration_id);
$configuration = [];
// Set the source plugin constant, source_base_path, for all migrations with
// a file entity destination.
// @todo https://www.drupal.org/node/2804611.
// Find a way to avoid having to set configuration here.
if ($definition['destination']['plugin'] === 'entity:file') {
// Use the private file path if the scheme property is set in the source
// plugin definition and is 'private' otherwise use the public file path.
$scheme = $definition['source']['scheme'] ?? NULL;
$base_path = ($scheme === 'private' && $config['source_private_file_path'])
? $config['source_private_file_path']
: $config['source_base_path'];
$configuration['source']['constants']['source_base_path'] = rtrim($base_path, '/');
}
/** @var \Drupal\migrate\Plugin\Migration $migration */
$migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id, $configuration);
if ($migration) {
static::$messages = new MigrateMessageCapture();
$executable = new MigrateExecutable($migration, static::$messages);
$migration_name = $migration->label() ? $migration->label() : $migration_id;
try {
$migration_status = $executable->import();
}
catch (\Exception $e) {
\Drupal::logger('migrate_drupal_ui')->error($e->getMessage());
$migration_status = MigrationInterface::RESULT_FAILED;
}
switch ($migration_status) {
case MigrationInterface::RESULT_COMPLETED:
// Store the number processed in the sandbox.
$context['sandbox']['num_processed'] += static::$numProcessed;
$message = new PluralTranslatableMarkup(
$context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @count items total)',
['@migration' => $migration_name]);
$context['sandbox']['messages'][] = (string) $message;
\Drupal::logger('migrate_drupal_ui')->notice($message);
$context['sandbox']['num_processed'] = 0;
$context['results']['successes']++;
// If the completed migration has any follow-up migrations, add them
// to the batch migrations.
// @see onPostImport()
if (!empty(static::$followUpMigrations)) {
foreach (static::$followUpMigrations as $migration_id => $migration) {
if (!in_array($migration_id, $context['sandbox']['migration_ids'], TRUE)) {
// Add the follow-up migration ID to the batch migration IDs for
// later execution.
$context['sandbox']['migration_ids'][] = $migration_id;
// Increase the number of migrations in the batch to update the
// progress bar and keep it accurate.
$context['sandbox']['max']++;
// Unset the follow-up migration to make sure it won't get added
// to the batch twice.
unset(static::$followUpMigrations[$migration_id]);
}
}
}
break;
case MigrationInterface::RESULT_INCOMPLETE:
$context['sandbox']['messages'][] = (string) new PluralTranslatableMarkup(
static::$numProcessed, 'Continuing with @migration (processed 1 item)', 'Continuing with @migration (processed @count items)',
['@migration' => $migration_name]);
$context['sandbox']['num_processed'] += static::$numProcessed;
break;
case MigrationInterface::RESULT_STOPPED:
$context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation stopped by request');
break;
case MigrationInterface::RESULT_FAILED:
$context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration failed', ['@migration' => $migration_name]);
$context['results']['failures']++;
\Drupal::logger('migrate_drupal_ui')->error('Operation on @migration failed', ['@migration' => $migration_name]);
break;
case MigrationInterface::RESULT_SKIPPED:
$context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
\Drupal::logger('migrate_drupal_ui')->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
break;
case MigrationInterface::RESULT_DISABLED:
// Skip silently if disabled.
break;
}
// Unless we're continuing on with this migration, take it off the list.
if ($migration_status != MigrationInterface::RESULT_INCOMPLETE) {
array_shift($context['sandbox']['migration_ids']);
$context['sandbox']['current']++;
}
// Add and log any captured messages.
foreach (static::$messages->getMessages() as $message) {
$context['sandbox']['messages'][] = (string) $message;
\Drupal::logger('migrate_drupal_ui')->error($message);
}
// Only display the last MESSAGE_LENGTH messages, in reverse order.
$message_count = count($context['sandbox']['messages']);
$context['message'] = '';
for ($index = max(0, $message_count - self::MESSAGE_LENGTH); $index < $message_count; $index++) {
$context['message'] = $context['sandbox']['messages'][$index] . "<br />\n" . $context['message'];
}
if ($message_count > self::MESSAGE_LENGTH) {
// Indicate there are earlier messages not displayed.
$context['message'] .= '&hellip;';
}
// At the top of the list, display the next one (which will be the one
// that is running while this message is visible).
if (!empty($context['sandbox']['migration_ids'])) {
$migration_id = reset($context['sandbox']['migration_ids']);
$migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id);
$migration_name = $migration->label() ? $migration->label() : $migration_id;
$context['message'] = (string) new TranslatableMarkup('Currently upgrading @migration (@current of @max total tasks)', [
'@migration' => $migration_name,
'@current' => $context['sandbox']['current'],
'@max' => $context['sandbox']['max'],
]) . "<br />\n" . $context['message'];
}
}
else {
array_shift($context['sandbox']['migration_ids']);
$context['sandbox']['current']++;
}
$context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max'];
}
/**
* Callback executed when the Migrate Upgrade Import batch process completes.
*
* @param bool $success
* TRUE if batch successfully completed.
* @param array $results
* Batch results.
* @param array $operations
* An array of methods run in the batch.
* @param string $elapsed
* The time to run the batch.
*/
public static function finished($success, $results, $operations, $elapsed) {
$successes = $results['successes'];
$failures = $results['failures'];
// If we had any successes log that for the user.
if ($successes > 0) {
\Drupal::messenger()->addStatus(\Drupal::translation()
->formatPlural($successes, 'Completed 1 upgrade task successfully', 'Completed @count upgrade tasks successfully'));
}
// If we had failures, log them and show the migration failed.
if ($failures > 0) {
\Drupal::messenger()->addError(\Drupal::translation()
->formatPlural($failures, '1 upgrade failed', '@count upgrades failed'));
\Drupal::messenger()->addError(t('Upgrade process not completed'));
}
else {
// Everything went off without a hitch. We may not have had successes
// but we didn't have failures so this is fine.
\Drupal::messenger()->addStatus(t('Congratulations, you upgraded Drupal!'));
}
if (\Drupal::moduleHandler()->moduleExists('dblog')) {
$url = Url::fromRoute('migrate_drupal_ui.log');
\Drupal::messenger()->addMessage(Link::fromTextAndUrl(new TranslatableMarkup('Review the detailed upgrade log'), $url), $failures ? 'error' : 'status');
}
}
/**
* Reacts to item import.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The post-save event.
*/
public static function onPostRowSave(MigratePostRowSaveEvent $event) {
// We want to interrupt this batch and start a fresh one.
$time = \Drupal::time();
if (($time->getCurrentTime() - $time->getRequestTime()) > static::$maxExecTime) {
$event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
}
}
/**
* Adds follow-up migrations.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The import event.
*/
public static function onPostImport(MigrateImportEvent $event) {
$migration = $event->getMigration();
if ($migration instanceof MigrationWithFollowUpInterface) {
// After the migration on which they depend has been successfully
// executed, the follow-up migrations are immediately added to the batch
// and removed from the $followUpMigrations property. This means that the
// $followUpMigrations property is always empty at this point and it's OK
// to override it with the next follow-up migrations.
static::$followUpMigrations = $migration->generateFollowUpMigrations();
}
}
/**
* Reacts to item deletion.
*
* @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event
* The post-save event.
*/
public static function onPostRowDelete(MigrateRowDeleteEvent $event) {
// We want to interrupt this batch and start a fresh one.
$time = \Drupal::time();
if (($time->getCurrentTime() - $time->getRequestTime()) > static::$maxExecTime) {
$event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
}
}
/**
* Counts up any map save events.
*
* @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
* The map event.
*/
public static function onMapSave(MigrateMapSaveEvent $event) {
static::$numProcessed++;
}
/**
* Counts up any map delete events.
*
* @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
* The map event.
*/
public static function onMapDelete(MigrateMapDeleteEvent $event) {
static::$numProcessed++;
}
/**
* Displays any messages being logged to the ID map.
*
* @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
* The message event.
*/
public static function onIdMapMessage(MigrateIdMapMessageEvent $event) {
if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE || $event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) {
$type = 'status';
}
else {
$type = 'error';
}
$migration_id = $event->getMigration()->getPluginId();
$source_id_string = implode(',', $event->getSourceIdValues());
$message = t('Migration @migration_id: Source ID @source_id: @message', [
'@migration_id' => $migration_id,
'@source_id' => $source_id_string,
'@message' => $event->getMessage(),
]);
static::$messages->display($message, $type);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\migrate_drupal_ui\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides controller methods for the migration.
*/
class MigrateController extends ControllerBase {
/**
* Sets a log filter and redirects to the log.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response object that may be returned by the controller.
*/
public function showLog(Request $request) {
// Sets both the session and the query parameter so that it works correctly
// with both the watchdog view and the fallback.
$request->getSession()->set('dblog_overview_filter', ['type' => ['migrate_drupal_ui' => 'migrate_drupal_ui']]);
return $this->redirect('dblog.overview', [], ['query' => ['type' => ['migrate_drupal_ui']]]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\migrate_drupal_ui\Controller;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\migrate\Controller\MigrateMessageController as BaseMessageController;
/**
* Provides controller methods for the Message form.
*/
class MigrateMessageController extends BaseMessageController {
/**
* Displays an overview of migrate messages.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function overview(): array {
$build = parent::overview();
$description['help'] = [
'#type' => 'item',
'#markup' => $this->t('The upgrade process may log messages about steps that require user action or errors. This page allows you to view these messages'),
];
$description['info'] = [
'#type' => 'item',
'#markup' => Link::fromTextAndUrl($this->t('Review the detailed upgrade log.'), Url::fromRoute('migrate_drupal_ui.log'))
->toString(),
];
return $description + $build;
}
}

View File

@@ -0,0 +1,470 @@
<?php
namespace Drupal\migrate_drupal_ui\Form;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\Exception\BadPluginDefinitionException;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use GuzzleHttp\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
// cspell:ignore idconflict
/**
* Migrate Upgrade database credential form.
*
* @internal
*/
class CredentialForm extends MigrateUpgradeFormBase {
/**
* The HTTP client to fetch the files with.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* An array of error information.
*
* @var array
*/
protected $errors = [];
/**
* CredentialForm constructor.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore_private
* The private tempstore factory service.
* @param \GuzzleHttp\ClientInterface $http_client
* A Guzzle client object.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(PrivateTempStoreFactory $tempstore_private, ClientInterface $http_client, ConfigFactoryInterface $config_factory, MigrationPluginManagerInterface $migration_plugin_manager, StateInterface $state) {
parent::__construct($config_factory, $migration_plugin_manager, $state, $tempstore_private);
$this->httpClient = $http_client;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('http_client'),
$container->get('config.factory'),
$container->get('plugin.manager.migration'),
$container->get('state')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'migrate_drupal_ui_credential_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
if ($this->store->get('step') != 'credential') {
return $this->restartUpgradeForm();
}
$form = parent::buildForm($form, $form_state);
$form['actions']['submit']['#value'] = $this->t('Review upgrade');
$form['#title'] = $this->t('Drupal Upgrade');
$drivers = $this->getDatabaseTypes();
$drivers_keys = array_keys($drivers);
$default_driver = current($drivers_keys);
$default_options = [];
$form['help'] = [
'#type' => 'item',
'#description' => $this->t('Provide the information to access the Drupal site you want to upgrade. Files can be imported into the upgraded site as well. See the <a href=":url">Upgrade documentation for more detailed instructions</a>.', [':url' => 'https://www.drupal.org/upgrade/migrate']),
];
$migrate_source_version = Settings::get('migrate_source_version') == '6' ? '6' : '7';
$form['version'] = [
'#type' => 'radios',
'#default_value' => $migrate_source_version,
'#title' => $this->t('Drupal version of the source site'),
'#options' => ['6' => $this->t('Drupal 6'), '7' => $this->t('Drupal 7')],
'#required' => TRUE,
];
$available_connections = array_diff(array_keys(Database::getAllConnectionInfo()), ['default']);
$options = array_combine($available_connections, $available_connections);
$migrate_source_connection = Settings::get('migrate_source_connection');
$preferred_connections = $migrate_source_connection
? ['migrate', $migrate_source_connection]
: ['migrate'];
$default_options = array_intersect($preferred_connections, $available_connections);
$form['source_connection'] = [
'#type' => 'select',
'#title' => $this->t('Source connection'),
'#options' => $options,
'#default_value' => array_pop($default_options),
'#empty_option' => $this->t('- User defined -'),
'#description' => $this->t('Choose one of the keys from the $databases array or else select "User defined" and enter database credentials.'),
'#access' => !empty($options),
];
$form['database'] = [
'#type' => 'details',
'#title' => $this->t('Source database'),
'#description' => $this->t('Provide credentials for the database of the Drupal site you want to upgrade.'),
'#open' => TRUE,
'#states' => [
'visible' => [
':input[name=source_connection]' => ['value' => ''],
],
],
];
$form['database']['driver'] = [
'#type' => 'radios',
'#title' => $this->t('Database type'),
'#required' => TRUE,
'#default_value' => $default_driver,
'#states' => [
'required' => [
':input[name=source_connection]' => ['value' => ''],
],
],
];
if (count($drivers) == 1) {
$form['database']['driver']['#disabled'] = TRUE;
}
// Add driver-specific configuration options.
foreach ($drivers as $key => $driver) {
$form['database']['driver']['#options'][$key] = $driver->name();
$form['database']['settings'][$key] = $driver->getFormOptions($default_options);
unset($form['database']['settings'][$key]['advanced_options']['prefix']['#description']);
// This is a multi-step form and is not rebuilt during submission so
// #limit_validation_errors is not used. The database and username fields
// for mysql and pgsql must not be required.
$form['database']['settings'][$key]['database']['#required'] = FALSE;
$form['database']['settings'][$key]['username']['#required'] = FALSE;
$form['database']['settings'][$key]['database']['#states'] = [
'required' => [
':input[name=source_connection]' => ['value' => ''],
':input[name=driver]' => ['value' => $key],
],
];
if (!str_ends_with($key, '\\sqlite')) {
$form['database']['settings'][$key]['username']['#states'] = [
'required' => [
':input[name=source_connection]' => ['value' => ''],
':input[name=driver]' => ['value' => $key],
],
];
$form['database']['settings'][$key]['password']['#states'] = [
'required' => [
':input[name=source_connection]' => ['value' => ''],
':input[name=driver]' => ['value' => $key],
],
];
}
$form['database']['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . $this->t('@driver_name settings', ['@driver_name' => $driver->name()]) . '</h2>';
$form['database']['settings'][$key]['#type'] = 'container';
$form['database']['settings'][$key]['#tree'] = TRUE;
$form['database']['settings'][$key]['advanced_options']['#parents'] = [$key];
$form['database']['settings'][$key]['#states'] = [
'visible' => [
':input[name=driver]' => ['value' => $key],
],
];
// Move the host fields out of advanced settings.
if (isset($form['database']['settings'][$key]['advanced_options']['host'])) {
$form['database']['settings'][$key]['host'] = $form['database']['settings'][$key]['advanced_options']['host'];
$form['database']['settings'][$key]['host']['#title'] = 'Database host';
$form['database']['settings'][$key]['host']['#weight'] = -1;
unset($form['database']['settings'][$key]['database']['#default_value']);
unset($form['database']['settings'][$key]['advanced_options']['host']);
}
}
$form['source'] = [
'#type' => 'details',
'#title' => $this->t('Source files'),
'#open' => TRUE,
];
$form['source']['d6_source_base_path'] = [
'#type' => 'textfield',
'#title' => $this->t('Document root for files'),
'#default_value' => Settings::get('migrate_file_public_path') ?? '',
'#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).'),
'#states' => [
'visible' => [
':input[name="version"]' => ['value' => '6'],
],
],
'#element_validate' => ['::validatePaths'],
];
$form['source']['source_base_path'] = [
'#type' => 'textfield',
'#title' => $this->t('Document root for public files'),
'#default_value' => Settings::get('migrate_file_public_path') ?? '',
'#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).'),
'#states' => [
'visible' => [
':input[name="version"]' => ['value' => '7'],
],
],
'#element_validate' => ['::validatePaths'],
];
$form['source']['source_private_file_path'] = [
'#type' => 'textfield',
'#title' => $this->t('Document root for private files'),
'#default_value' => Settings::get('migrate_file_private_path') ?? '',
'#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). Leave blank to use the same value as Public files directory.'),
'#states' => [
'visible' => [
':input[name="version"]' => ['value' => '7'],
],
],
'#element_validate' => ['::validatePaths'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$connection = NULL;
$source_connection = $form_state->getValue('source_connection');
if ($source_connection) {
$info = Database::getConnectionInfo($source_connection);
$database = reset($info);
}
else {
// Retrieve the database driver from the form, use reflection to get the
// namespace, and then construct a valid database array the same as in
// settings.php.
$driver = $form_state->getValue('driver');
$drivers = $this->getDatabaseTypes();
$reflection = new \ReflectionClass($drivers[$driver]);
$install_namespace = $reflection->getNamespaceName();
$database = $form_state->getValue($driver);
// Cut the trailing \Install from namespace.
$database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
$database['driver'] = $driver;
// Validate the driver settings and just end here if we have any issues.
if ($errors = $drivers[$driver]->validateDatabaseSettings($database)) {
foreach ($errors as $name => $message) {
$this->errors[$name] = $message;
}
}
}
// Check the connection with the source database.
$error_key = $database['driver'] . '][database';
if (!$this->errors) {
try {
$connection = $this->getConnection($database);
}
catch (\Exception $e) {
$msg = $this->t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', ['%error' => $e->getMessage()]);
$this->errors[$error_key] = $msg;
}
}
// Check that the source database is not the site database.
if (!$this->errors) {
$options = array_flip(['database', 'host', 'port', 'prefix', 'namespace']);
$source = array_map('strval', array_intersect_key(
$connection->getConnectionOptions(),
$options,
));
$destination = array_map('strval', array_intersect_key(
Database::getConnection()->getConnectionOptions(),
$options,
));
ksort($source);
ksort($destination);
if ($source === $destination) {
$this->errors[$error_key] = $this->t('Enter credentials for the database of the Drupal site you want to upgrade, not the new site.');
}
}
// Get the Drupal version of the source database so it can be validated.
if (!$this->errors) {
$version = (string) $this->getLegacyDrupalVersion($connection);
if (!$version) {
$this->errors[$error_key] = $this->t('Source database does not contain a recognizable Drupal version.');
}
elseif ($version !== (string) $form_state->getValue('version')) {
$this->errors['version'] = $this->t('Source database is Drupal version @version but version @selected was selected.',
[
'@version' => $version,
'@selected' => $form_state->getValue('version'),
]);
}
}
// Setup migrations and save form data to private store.
if (!$this->errors) {
try {
$this->setupMigrations($connection, $version, $database, $form_state);
}
catch (BadPluginDefinitionException $e) {
// BadPluginDefinitionException occurs if the source_module is not
// defined, which happens during testing.
$this->errors[$error_key] = $e->getMessage();
}
catch (RequirementsException $e) {
$this->errors[$error_key] = $e->getMessage();
}
}
// Display all errors as a list of items.
if ($this->errors) {
$form_state->setError($form, $this->t('<h3>Resolve all issues below to continue the upgrade.</h3>'));
foreach ($this->errors as $name => $message) {
$form_state->setErrorByName($name, $message);
}
}
}
/**
* The #element_validate handler for the source path elements.
*
* Ensures that entered path can be read.
*/
public function validatePaths($element, FormStateInterface $form_state) {
$version = $form_state->getValue('version');
// Only validate the paths relevant to the legacy Drupal version.
if (($version !== '7')
&& ($element['#name'] == 'source_base_path' || $element['#name'] == 'source_private_file_path')) {
return;
}
if ($version !== '6' && ($element['#name'] == 'd6_source_base_path')) {
return;
}
if ($source = $element['#value']) {
$msg = $this->t('Failed to read from @title.', ['@title' => $element['#title']]);
if (UrlHelper::isExternal($source)) {
try {
$this->httpClient->head($source);
}
catch (ClientExceptionInterface $e) {
$msg .= ' ' . $this->t('The server reports the following message: %error.', ['%error' => $e->getMessage()]);
$this->errors[$element['#name']] = $msg;
}
}
elseif (!file_exists($source) || (!is_dir($source)) || (!is_readable($source))) {
$this->errors[$element['#name']] = $msg;
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->store->set('step', 'idconflict');
$form_state->setRedirect('migrate_drupal_ui.upgrade_idconflict');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Review upgrade');
}
/**
* Returns all supported database driver installer objects.
*
* @return \Drupal\Core\Database\Install\Tasks[]
* An array of available database driver installer objects.
*/
protected function getDatabaseTypes() {
// Make sure the install API is available.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
$database_types = [];
foreach (Database::getDriverList()->getInstallableList() as $name => $driver) {
$database_types[$name] = $driver->getInstallTasks();
}
return $database_types;
}
/**
* Gets and stores information for this migration in temporary store.
*
* Gets all the migrations, converts each to an array and stores it in the
* form state. The source base path for public and private files is also
* put into form state.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection used.
* @param string $version
* The Drupal version.
* @param array $database
* Database array representing the source Drupal database.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @throws \Drupal\Core\TempStore\TempStoreException
* Thrown when a lock for the backend storage could not be acquired.
*/
protected function setupMigrations(Connection $connection, $version, array $database, FormStateInterface $form_state) {
$this->createDatabaseStateSettings($database, $version);
$migrations = $this->getMigrations('migrate_drupal_' . $version, $version);
// Get the system data from source database.
$system_data = $this->getSystemData($connection);
// Convert the migration object into array
// so that it can be stored in form storage.
$migration_array = [];
foreach ($migrations as $migration) {
$migration_array[$migration->id()] = $migration->label();
}
// Store information in the private store.
$this->store->set('version', $version);
$this->store->set('migrations', $migration_array);
if ($version == 6) {
$this->store->set('source_base_path', $form_state->getValue('d6_source_base_path'));
}
else {
$this->store->set('source_base_path', $form_state->getValue('source_base_path'));
}
$this->store->set('source_private_file_path', $form_state->getValue('source_private_file_path'));
// Store the retrieved system data in the private store.
$this->store->set('system_data', $system_data);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Drupal\migrate_drupal_ui\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\migrate\Audit\IdAuditor;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
// cspell:ignore idconflict
/**
* Migrate Upgrade Id Conflict form.
*
* @internal
*/
class IdConflictForm extends MigrateUpgradeFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'migrate_drupal_ui_idconflict_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Get all the data needed for this form.
$migrations = $this->store->get('migrations');
// If data is missing or this is the wrong step, start over.
if (!$migrations || ($this->store->get('step') != 'idconflict')) {
return $this->restartUpgradeForm();
}
$migration_ids = array_keys($migrations);
// Check if there are conflicts. If none, just skip this form!
$migrations = $this->migrationPluginManager->createInstances($migration_ids);
$translated_content_conflicts = $content_conflicts = [];
$results = (new IdAuditor())->auditMultiple($migrations);
/** @var \Drupal\migrate\Audit\AuditResult $result */
foreach ($results as $result) {
$destination = $result->getMigration()->getDestinationPlugin();
if ($destination instanceof EntityContentBase && $destination->isTranslationDestination()) {
// Translations are not yet supported by the audit system. For now, we
// only warn the user to be cautious when migrating translated content.
// I18n support should be added in https://www.drupal.org/node/2905759.
$translated_content_conflicts[] = $result;
}
elseif (!$result->passed()) {
$content_conflicts[] = $result;
}
}
if ($content_conflicts || $translated_content_conflicts) {
$this->messenger()->addWarning($this->t('WARNING: Content may be overwritten on your new site.'));
$form = parent::buildForm($form, $form_state);
$form['#title'] = $this->t('Upgrade analysis report');
if ($content_conflicts) {
$form = $this->conflictsForm($form, $content_conflicts);
}
if ($translated_content_conflicts) {
$form = $this->i18nWarningForm($form, $translated_content_conflicts);
}
return $form;
}
else {
$this->store->set('step', 'review');
return $this->redirect('migrate_drupal_ui.upgrade_review');
}
}
/**
* Build the markup for conflict warnings.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\migrate\Audit\AuditResult[] $conflicts
* The failing audit results.
*
* @return array
* The form structure.
*/
protected function conflictsForm(array &$form, array $conflicts) {
$form['conflicts'] = [
'#title' => $this->t('There is conflicting content of these types:'),
'#theme' => 'item_list',
'#items' => $this->formatConflicts($conflicts),
];
$form['warning'] = [
'#type' => 'markup',
'#markup' => '<p>' . $this->t('It looks like you have content on your new site which <strong>may be overwritten</strong> if you continue to run this upgrade. The upgrade should be performed on a clean Drupal @version installation. For more information see the <a target="_blank" href=":id-conflicts-handbook">upgrade handbook</a>.', [
'@version' => $this->destinationSiteVersion,
':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_conflicts',
]) . '</p>',
];
return $form;
}
/**
* Formats a set of failing audit results as strings.
*
* Each string is the label of the destination plugin of the migration that
* failed the audit, keyed by the destination plugin ID in order to prevent
* duplication.
*
* @param \Drupal\migrate\Audit\AuditResult[] $conflicts
* The failing audit results.
*
* @return string[]
* The formatted audit results.
*/
protected function formatConflicts(array $conflicts) {
$items = [];
foreach ($conflicts as $conflict) {
$definition = $conflict->getMigration()->getDestinationPlugin()->getPluginDefinition();
$id = $definition['id'];
$items[$id] = $definition['label'];
}
sort($items, SORT_STRING);
return array_unique($items);
}
/**
* Build the markup for i18n warnings.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\migrate\Audit\AuditResult[] $conflicts
* The failing audit results.
*
* @return array
* The form structure.
*/
protected function i18nWarningForm(array &$form, array $conflicts) {
$form['i18n'] = [
'#title' => $this->t('Check whether there is translated content of these types:'),
'#theme' => 'item_list',
'#items' => $this->formatConflicts($conflicts),
];
$form['i18n_warning'] = [
'#type' => 'markup',
'#markup' => '<p>' . $this->t('Possible ID conflicts for translations are not automatically detected in the current version of Drupal. Refer to the <a target="_blank" href=":id-conflicts-handbook">Upgrading Drupal handbook</a> for instructions on how to avoid ID conflicts with translated content.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/upgrading-drupal/upgrading-from-drupal-6-or-drupal-7/known-issues-when-upgrading-from-drupal-6-or-7#id_conflicts']) . '</p>',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->store->set('step', 'review');
$form_state->setRedirect('migrate_drupal_ui.upgrade_review');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('I acknowledge I may lose data. Continue anyway.');
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\migrate_drupal_ui\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Migrate Upgrade Incremental form.
*
* @internal
*/
class IncrementalForm extends MigrateUpgradeFormBase {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* IncrementalForm constructor.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore_private
* The private tempstore factory service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager service.
*/
public function __construct(StateInterface $state, DateFormatterInterface $date_formatter, PrivateTempStoreFactory $tempstore_private, ConfigFactoryInterface $config_factory, MigrationPluginManagerInterface $migration_plugin_manager) {
parent::__construct($config_factory, $migration_plugin_manager, $state, $tempstore_private);
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('state'),
$container->get('date.formatter'),
$container->get('tempstore.private'),
$container->get('config.factory'),
$container->get('plugin.manager.migration')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'migrate_drupal_ui_incremental_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Get all the data needed for this form.
$date_performed = $this->state->get('migrate_drupal_ui.performed');
// If data is missing or this is the wrong step, start over.
if (!$date_performed || $this->store->get('step') != 'incremental') {
return $this->restartUpgradeForm();
}
$form = parent::buildForm($form, $form_state);
$form['#title'] = $this->t('Upgrade');
// @todo Add back support for rollbacks.
// https://www.drupal.org/node/2687849
$form['upgrade_option_item'] = [
'#type' => 'item',
'#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 @version. Rollbacks are not yet supported through the user interface. For more information, see the <a href=":url">upgrading handbook</a>.', [
'@version' => $this->destinationSiteVersion,
':url' => 'https://www.drupal.org/upgrade/migrate',
]),
'#description' => $this->t('Last upgrade: @date', ['@date' => $this->dateFormatter->format($date_performed)]),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->store->set('step', 'credential');
$form_state->setRedirect('migrate_drupal_ui.upgrade_credential');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Import new configuration and content from old site');
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\migrate_drupal_ui\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_drupal\MigrationConfigurationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form base for the Migrate Upgrade UI.
*/
abstract class MigrateUpgradeFormBase extends FormBase {
use MigrationConfigurationTrait;
/**
* Private temporary storage.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $store;
/**
* The destination site major version.
*
* @var string
*/
protected $destinationSiteVersion;
/**
* Constructs the Migrate Upgrade Form Base.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore_private
* The private tempstore factory service.
*/
public function __construct(ConfigFactoryInterface $config_factory, MigrationPluginManagerInterface $migration_plugin_manager, StateInterface $state, PrivateTempStoreFactory $tempstore_private) {
$this->configFactory = $config_factory;
$this->migrationPluginManager = $migration_plugin_manager;
$this->state = $state;
$this->store = $tempstore_private->get('migrate_drupal_ui');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('plugin.manager.migration'),
$container->get('state'),
$container->get('tempstore.private')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Get the current major version.
[$this->destinationSiteVersion] = explode('.', \Drupal::VERSION, 2);
$form = [];
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#button_type' => 'primary',
'#weight' => 10,
];
return $form;
}
/**
* Helper to redirect to the Overview form.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response object that may be returned by the controller.
*
* @throws \Drupal\Core\TempStore\TempStoreException
* Thrown when a lock for the backend storage could not be acquired.
*/
protected function restartUpgradeForm() {
$this->store->set('step', 'overview');
return $this->redirect('migrate_drupal_ui.upgrade');
}
/**
* Returns a caption for the button that confirms the action.
*
* @return string
* The form confirmation text.
*/
abstract protected function getConfirmText();
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Drupal\migrate_drupal_ui\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Migrate Upgrade Overview form.
*
* @internal
*/
class OverviewForm extends MigrateUpgradeFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'migrate_drupal_ui_overview_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// If an upgrade has already been performed, redirect to the incremental
// form.
if ($this->state->get('migrate_drupal_ui.performed')) {
$this->store->set('step', 'incremental');
return $this->redirect('migrate_drupal_ui.upgrade_incremental');
}
$form = parent::buildForm($form, $form_state);
$form['#title'] = $this->t('Upgrade');
$form['info_header'] = [
'#markup' => '<p>' . $this->t('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal @version. See the <a href=":url">Drupal site upgrades handbook</a> for more information.', [
'@version' => $this->destinationSiteVersion,
':url' => 'https://www.drupal.org/upgrade/migrate',
]),
];
$form['legend']['#markup'] = '';
$form['legend']['#markup'] .= '<h3>' . $this->t('Definitions') . '</h3>';
$form['legend']['#markup'] .= '<dl>';
$form['legend']['#markup'] .= '<dt>' . $this->t('Old site') . '</dt>';
$form['legend']['#markup'] .= '<dd>' . $this->t('The site you want to upgrade.') . '</dd>';
$form['legend']['#markup'] .= '<dt>' . $this->t('New site') . '</dt>';
$form['legend']['#markup'] .= '<dd>' . $this->t('This empty Drupal @version installation you will import the old site to.', ['@version' => $this->destinationSiteVersion]) . '</dd>';
$form['legend']['#markup'] .= '</dl>';
$info[] = $this->t('Make sure that <strong>access to the database</strong> for the old site is available from this new site.');
$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.');
$info[] = $this->t('<strong>Install 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 install the Book module on this new site so that the existing data can be imported to it.');
$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>.', [
':url' => 'https://www.drupal.org/docs/8/upgrade/preparing-an-upgrade#do_not_create_content',
]);
$info[] = $this->t('Put this site into <a href=":url">maintenance mode</a>.', [
':url' => Url::fromRoute('system.site_maintenance_mode')
->toString(TRUE)
->getGeneratedUrl(),
]);
$form['info'] = [
'#theme' => 'item_list',
'#title' => $this->t('Preparation steps'),
'#list_type' => 'ol',
'#items' => $info,
];
$form['info_footer'] = [
'#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.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->store->set('step', 'credential');
$form_state->setRedirect('migrate_drupal_ui.upgrade_credential');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Continue');
}
}

View File

@@ -0,0 +1,345 @@
<?php
namespace Drupal\migrate_drupal_ui\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate_drupal\MigrationState;
use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Migrate Upgrade review form.
*
* This confirmation form provides the user with a summary of all the modules
* enabled on the source site and whether they will be upgraded or not. Data
* from a module's .migrate_drupal.yml file and all the migration plugins
* (source, destination and field) for each enabled Drupal 8 module are used to
* decide the migration status for each enabled module on the source site.
*
* The migration status displayed on the Review page is a list of all the
* enabled modules on the source site divided into two categories, those that
* will not be upgraded and those that will be upgraded. The intention is to
* provide the admin with enough information to decide if it is OK to proceed
* with the upgrade.
*
* @internal
*/
class ReviewForm extends MigrateUpgradeFormBase {
use DeprecatedServicePropertyTrait;
/**
* The service properties that should raise a deprecation error.
*/
private array $deprecatedProperties = ['moduleHandler' => 'module_handler'];
/**
* The migrations.
*
* @var \Drupal\migrate\Plugin\MigrationInterface[]
*/
protected $migrations;
/**
* Migration state service.
*
* @var \Drupal\migrate_drupal\MigrationState
*/
protected $migrationState;
/**
* Module extension list.
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* Source system data set in buildForm().
*
* @var array
*/
protected $systemData;
/**
* ReviewForm constructor.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager service.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore_private
* The private tempstore factory service.
* @param \Drupal\migrate_drupal\MigrationState $migrationState
* Migration state service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Extension\ModuleExtensionList|\Drupal\Core\Extension\ModuleHandlerInterface $module_extension_list
* The module extension list.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(
StateInterface $state,
MigrationPluginManagerInterface $migration_plugin_manager,
PrivateTempStoreFactory $tempstore_private,
MigrationState $migrationState,
ConfigFactoryInterface $config_factory,
ModuleExtensionList|ModuleHandlerInterface $module_extension_list,
protected ?TimeInterface $time = NULL,
) {
parent::__construct($config_factory, $migration_plugin_manager, $state, $tempstore_private);
$this->migrationState = $migrationState;
if ($this->time === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
$this->time = \Drupal::service('datetime.time');
}
if ($module_extension_list instanceof ModuleHandlerInterface) {
@trigger_error('Calling ' . __METHOD__ . '() with the $module_extension_list argument as ModuleHandlerInterface is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$module_extension_list = \Drupal::service('extension.list.module');
}
$this->moduleExtensionList = $module_extension_list;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('state'),
$container->get('plugin.manager.migration'),
$container->get('tempstore.private'),
$container->get('migrate_drupal.migration_state'),
$container->get('config.factory'),
$container->get('extension.list.module'),
$container->get('datetime.time'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'migrate_drupal_ui_review_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Get all the data needed for this form.
$version = $this->store->get('version');
$this->migrations = $this->store->get('migrations');
// Fetch the source system data at the first opportunity.
$this->systemData = $this->store->get('system_data');
// If data is missing or this is the wrong step, start over.
if (!$version || !$this->migrations || !$this->systemData ||
($this->store->get('step') != 'review')) {
return $this->restartUpgradeForm();
}
$form = parent::buildForm($form, $form_state);
$form['#title'] = $this->t('What will be upgraded?');
$migrations = $this->migrationPluginManager->createInstances(array_keys($this->store->get('migrations')));
// Get the upgrade states for the source modules.
$display = $this->migrationState->getUpgradeStates($version, $this->systemData, $migrations);
// Missing migrations.
$missing_module_list = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Modules that will not be upgraded'),
'#summary_attributes' => ['id' => ['error']],
'#description' => $this->t("The new site is missing modules corresponding to the old site's modules. Unless they are installed prior to the upgrade, configuration and/or content needed by them will not be available on your new site. <a href=':review'>Read the checklist</a> to help decide what to do.", [':review' => 'https://www.drupal.org/docs/8/upgrade/upgrade-using-web-browser#pre-upgrade-analysis']),
'#weight' => 2,
];
$missing_module_list['module_list'] = [
'#type' => 'table',
'#header' => [
$this->t('Drupal @version module name', ['@version' => $version]),
$this->t('Drupal @version machine name', ['@version' => $version]),
$this->t('Drupal @version', ['@version' => $this->destinationSiteVersion]),
],
];
$missing_count = 0;
if (isset($display[MigrationState::NOT_FINISHED])) {
$output = $this->prepareOutput($display[MigrationState::NOT_FINISHED]);
foreach ($output as $data) {
$missing_count++;
// Get the migration status for each source module, if a module of the
// same name exists on the destination site.
$missing_module_list['module_list']['#rows'][] = [
[
'data' => $data['source_module_name'],
'class' => ['upgrade-analysis-report__status-icon', 'upgrade-analysis-report__status-icon--error'],
],
$data['source_machine_name'],
$data['destination'],
];
}
}
// Available migrations.
$available_module_list = [
'#type' => 'details',
'#title' => $this->t('Modules that will be upgraded'),
'#summary_attributes' => ['id' => ['checked']],
'#weight' => 4,
];
$available_module_list['module_list'] = [
'#type' => 'table',
'#header' => [
$this->t('Drupal @version module name', ['@version' => $version]),
$this->t('Drupal @version machine name', ['@version' => $version]),
$this->t('Drupal @version', ['@version' => $this->destinationSiteVersion]),
],
];
$available_count = 0;
if (isset($display[MigrationState::FINISHED])) {
$output = $this->prepareOutput($display[MigrationState::FINISHED]);
foreach ($output as $data) {
$available_count++;
$available_module_list['module_list']['#rows'][] = [
[
'data' => $data['source_module_name'],
'class' => ['upgrade-analysis-report__status-icon', 'upgrade-analysis-report__status-icon--checked'],
],
$data['source_machine_name'],
$data['destination'],
];
}
}
$counters = [];
$general_info = [];
if ($missing_count) {
$counters[] = [
'#theme' => 'status_report_counter',
'#amount' => $missing_count,
'#text' => $this->formatPlural($missing_count, 'Module will not be upgraded', 'Modules will not be upgraded'),
'#severity' => 'error',
'#weight' => 0,
];
$general_info[] = $missing_module_list;
}
if ($available_count) {
$counters[] = [
'#theme' => 'status_report_counter',
'#amount' => $available_count,
'#text' => $this->formatPlural($available_count, 'Module will be upgraded', 'Modules will be upgraded'),
'#severity' => 'checked',
'#weight' => 1,
];
$general_info[] = $available_module_list;
}
$form['status_report_page'] = [
'#theme' => 'status_report_page',
'#counters' => $counters,
'#general_info' => $general_info,
];
$form['#attached']['library'][] = 'migrate_drupal_ui/base';
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config['source_base_path'] = $this->store->get('source_base_path');
$config['source_private_file_path'] = $this->store->get('source_private_file_path');
$batch_builder = (new BatchBuilder())
->setTitle($this->t('Running upgrade'))
->setProgressMessage('')
->addOperation([
MigrateUpgradeImportBatch::class,
'run',
], [array_keys($this->migrations), $config])
->setFinishCallback([MigrateUpgradeImportBatch::class, 'finished']);
batch_set($batch_builder->toArray());
$form_state->setRedirect('<front>');
$this->store->set('step', 'overview');
$this->state->set('migrate_drupal_ui.performed', $this->time->getRequestTime());
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Perform upgrade');
}
/**
* Prepare the migration state data for output.
*
* Each source and destination module_name is changed to the human-readable
* name, the destination modules are put into a CSV format, and everything is
* sorted.
*
* @param string[] $migration_state
* An array where the keys are machine names of modules on
* the source site. Values are lists of machine names of modules on the
* destination site, in CSV format.
*
* @return string[][]
* An indexed array of arrays that contain module data, sorted by the source
* module name. Each sub-array contains the source module name, the source
* module machine name, and the destination module names in a sorted CSV
* format.
*/
protected function prepareOutput(array $migration_state) {
$output = [];
foreach ($migration_state as $source_machine_name => $destination_modules) {
$data = NULL;
if (isset($this->systemData['module'][$source_machine_name]['info'])) {
$data = unserialize($this->systemData['module'][$source_machine_name]['info']);
}
$source_module_name = $data['name'] ?? $source_machine_name;
// Get the names of all the destination modules.
$destination_module_names = [];
if (!empty($destination_modules)) {
$destination_modules = explode(', ', $destination_modules);
foreach ($destination_modules as $destination_module) {
if ($destination_module === 'core') {
$destination_module_names[] = 'Core';
}
else {
try {
$destination_module_names[] = $this->moduleExtensionList->getName($destination_module);
}
catch (UnknownExtensionException $e) {
$destination_module_names[] = $destination_module;
}
}
}
}
sort($destination_module_names);
$output[$source_machine_name] = [
'source_module_name' => $source_module_name,
'source_machine_name' => $source_machine_name,
'destination' => implode(', ', $destination_module_names),
];
}
usort($output, function ($a, $b) {
return strcmp($a['source_module_name'], $b['source_module_name']);
});
return $output;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\migrate_drupal_ui;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Session\AccountInterface;
/**
* Checks access for migrate_drupal_ui routes.
*
* The Migrate Drupal UI can only be used by user 1. This is because any other
* user might have different permissions on the source and target site.
*
* This class is designed to be used with '_custom_access' route requirement.
*
* @see \Drupal\Core\Access\CustomAccessCheck
*/
class MigrateAccessCheck {
/**
* Checks if the user is user 1 and grants access if so.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user account.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
public function checkAccess(AccountInterface $account) {
// The access result is uncacheable because it is just limiting access to
// the migrate UI which is not worth caching.
return AccessResultAllowed::allowedIf((int) $account->id() === 1)->mergeCacheMaxAge(0);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\migrate_drupal_ui\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Sets the controller for Migrate Message route.
*/
class MigrateDrupalUiRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
$route = $collection->get('migrate.messages');
if ($route) {
$route->setDefault('_controller', '\Drupal\migrate_drupal_ui\Controller\MigrateMessageController::overview');
}
}
}

View File

@@ -0,0 +1,10 @@
name: Test display of migrate message
type: module
description: Tests the display of migrate messages.
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,11 @@
id: d7_menu_test
label: Test display of upgrade messages
migration_tags:
- Drupal 7
source:
plugin: d7_menu_test
process:
message: site_offline_message
destination:
plugin: config
config_name: test.settings

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate_drupal_message_test\Plugin\migrate\source;
use Drupal\system\Plugin\migrate\source\Menu;
/**
* Source plugin with a source id removed from the array returned by fields().
*
* @MigrateSource(
* id = "d7_menu_test",
* source_module = "menu"
* )
*/
class MenuTest extends Menu {
/**
* {@inheritdoc}
*/
public function fields() {
$fields = parent::fields();
unset($fields['menu_name']);
return $fields;
}
}

View File

@@ -0,0 +1,10 @@
name: 'Migration provider missing'
type: module
description: 'Add a migration missing a source and destination migration provider and a source plugin missing a migration provider.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,13 @@
id: migration_provider_no_annotation
label: Missing source annotation
migration_tags:
- Drupal 6
- Drupal 7
source:
# Test plugin without a source_module annotation
plugin: no_source_module
process:
message: site_offline_message
destination:
plugin: config
config_name: test.settings

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\migration_provider_test\Plugin\migrate\source;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* A test source plugin without a source_module.
*
* @MigrateSource(
* id = "no_source_module",
* )
*/
class NoSourceModule extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
throw new \BadMethodCallException('This method should never be called');
}
/**
* {@inheritdoc}
*/
public function fields() {
throw new \BadMethodCallException('This method should never be called');
}
/**
* {@inheritdoc}
*/
public function getIds() {
throw new \BadMethodCallException('This method should never be called');
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
// cspell:ignore drupalmysqldriverdatabasemysql
/**
* Test the credential form for both Drupal 6 and Drupal 7 sources.
*
* The credential form is tested with incorrect credentials, correct
* credentials, and incorrect file paths.
*
* @group migrate_drupal_ui
*/
class CredentialFormTest extends MigrateUpgradeTestBase {
use CreateTestContentEntitiesTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal_ui'];
/**
* Test the credential form.
*
* @dataProvider providerCredentialForm
*/
public function testCredentialFrom($path_to_database): void {
$this->loadFixture($this->getModulePath('migrate_drupal') . $path_to_database);
$session = $this->assertSession();
// Get valid credentials.
$edit = $this->getCredentials();
$version = $edit['version'];
$edits = $this->translatePostValues($edit);
$this->drupalGet('/upgrade');
$session->responseContains("Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal $this->destinationSiteVersion.");
$this->submitForm([], 'Continue');
$session->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.');
$session->fieldExists('edit-drupalmysqldriverdatabasemysql-host');
// Check error message when the source database and the site database are
// the same.
$site_edit = $this->getDestinationSiteCredentials();
$site_edits = $this->translatePostValues($site_edit);
$this->submitForm($site_edits, 'Review upgrade');
$session->pageTextContains('Resolve all issues below to continue the upgrade.');
$session->pageTextContains('Enter credentials for the database of the Drupal site you want to upgrade, not the new site.');
// Ensure submitting the form with invalid database credentials gives us a
// nice warning.
$this->submitForm([$edit['driver'] . '[database]' => 'wrong'] + $edits, 'Review upgrade');
$session->pageTextContains('Resolve all issues below to continue the upgrade.');
// Resubmit with correct credentials.
$this->submitForm($edits, 'Review upgrade');
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Restart the upgrade and test the file source paths.
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
if ($version == 6) {
$paths['d6_source_base_path'] = DRUPAL_ROOT . '/wrong-path';
}
else {
$paths['source_base_path'] = 'https://example.com/wrong-path';
$paths['source_private_file_path'] = DRUPAL_ROOT . '/wrong-path';
}
$this->submitForm($paths + $edits, 'Review upgrade');
if ($version == 6) {
$session->responseContains('Failed to read from Document root for files.');
}
else {
$session->responseContains('Failed to read from Document root for public files.');
$session->responseContains('Failed to read from Document root for private files.');
}
}
/**
* Data provider for testCredentialForm.
*/
public static function providerCredentialForm() {
return [
[
'path_to_database' => '/tests/fixtures/drupal6.php',
],
[
'path_to_database' => '/tests/fixtures/drupal7.php',
],
];
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
$version = $this->getLegacyDrupalVersion($this->sourceDatabase);
return __DIR__ . '/d' . $version . '/files';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
/**
* Creates an array of destination site credentials for the Credential form.
*
* Before submitting to the Credential form the array must be processed by
* BrowserTestBase::translatePostValues() before submitting.
*
* @return array
* An array of values suitable for BrowserTestBase::translatePostValues().
*
* @see \Drupal\migrate_drupal_ui\Form\CredentialForm
*/
protected function getDestinationSiteCredentials() {
$connection_options = \Drupal::database()->getConnectionOptions();
$version = $this->getLegacyDrupalVersion($this->sourceDatabase);
$driver = $connection_options['driver'];
// Use the driver connection form to get the correct options out of the
// database settings. This supports all of the databases we test against.
$drivers = Database::getDriverList()->getInstallableList();
$form = $drivers[$driver]->getInstallTasks()->getFormOptions($connection_options);
$connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']);
// Remove isolation_level since that option is not configurable in the UI.
unset($connection_options['isolation_level']);
$edit = [
$driver => $connection_options,
'version' => $version,
];
if (count($drivers) !== 1) {
$edit['driver'] = $driver;
}
return $edit;
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for migrate_drupal_ui.
*
* @group migrate_drupal_ui
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that only user 1 can access the migrate UI.
*
* @group migrate_drupal_ui
*/
class MigrateAccessTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate_drupal_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that only user 1 can access the migrate UI.
*/
public function testAccess(): void {
$this->drupalLogin($this->rootUser);
$this->drupalGet('upgrade');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Upgrade');
$user = $this->createUser(['administer software updates']);
$this->drupalLogin($user);
$this->drupalGet('upgrade');
$this->assertSession()->statusCodeEquals(403);
$this->assertSession()->pageTextNotContains('Upgrade');
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\views\Entity\View;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for the MigrateController class.
*
* @group migrate_drupal_ui
*/
class MigrateControllerTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'dblog',
'migrate_drupal_ui',
'views_ui',
];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Log in as user 1. Migrations in the UI can only be performed as user 1.
$this->drupalLogin($this->rootUser);
// Create a migrate message for testing purposes.
\Drupal::logger('migrate_drupal_ui')->notice('A test message');
}
/**
* Tests the upgrade report with the view enabled, disabled and uninstalled.
*/
public function testUpgradeReport(): void {
$session = $this->assertSession();
$this->assertTrue(View::load('watchdog')->status(), 'Watchdog view is enabled');
// Tests redirection to report page when the watchdog view is enabled.
$this->drupalGet('admin/reports/upgrade');
$session->optionExists('type[]', 'migrate_drupal_ui')->isSelected();
$session->pageTextContainsOnce('A test message');
// Disable the watchdog view.
$this->drupalGet('admin/structure/views');
$this->assertTrue($this->clickViewsOperationsLink('Disable', '/watchdog/'));
$session->statusCodeEquals(200);
// Tests redirection to report page when the watchdog view is disabled.
$this->drupalGet('admin/reports/upgrade');
$session->optionExists('type[]', 'migrate_drupal_ui')->isSelected();
$session->pageTextContainsOnce('A test message');
\Drupal::service('module_installer')->uninstall(['views_ui', 'views']);
// Tests redirection to report page when views is uninstalled.
$this->drupalGet('admin/reports/upgrade');
$session->optionExists('type[]', 'migrate_drupal_ui')->isSelected();
$session->pageTextContainsOnce('A test message');
}
/**
* Clicks a view link to perform an operation.
*
* @param string $label
* Text between the anchor tags of the link.
* @param string $href_part
* A unique string that is expected to occur within the href of the link.
*
* @return bool
* TRUE when link found and clicked, otherwise FALSE.
*/
public function clickViewsOperationsLink($label, $href_part) {
$links = $this->xpath('//a[normalize-space(text())=:label]', [':label' => (string) $label]);
foreach ($links as $link_index => $link) {
$position = strpos($link->getAttribute('href'), $href_part);
if ($position !== FALSE) {
$index = $link_index;
$this->clickLink((string) $label, $index);
return TRUE;
}
}
return FALSE;
}
}

View File

@@ -0,0 +1,564 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Core\Database\Database;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Tests for the MigrateController class.
*
* @group migrate_drupal_ui
*/
class MigrateMessageControllerTest extends MigrateUpgradeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'menu_link_content',
'message_test',
'migrate_drupal_message_test',
'migrate_drupal_ui',
'system',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Migration IDs.
*
* @var string[]
*/
protected $migrationIds = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Migrations that access the source database in fields().
$this->migrationIds = [
'd6_menu',
'd6_menu_links',
'd6_profile_values',
'd6_user',
'd7_menu',
'd7_menu_links',
'd7_menu_test',
'd7_user',
];
$user = $this->createUser(['view migration messages']);
$this->drupalLogin($user);
$this->database = \Drupal::database();
}
/**
* Tests the overview page for migrate messages.
*
* Tests the overview page with the following scenarios;
* - No source database connection or message tables.
* - No source database connection with message tables.
* - A source database connection with message tables.
*/
public function testOverview(): void {
$session = $this->assertSession();
// First, test with no source database or message tables.
$this->drupalGet('/admin/reports/migration-messages');
$session->titleEquals('Migration messages | Drupal');
$session->pageTextContainsOnce('The upgrade process may log messages about steps that require user action or errors. This page allows you to view these messages');
$session->pageTextContainsOnce('There are no migration message tables.');
// Create map and message tables.
$this->createMigrateTables($this->migrationIds);
// Test overview with no source database connection and with message tables.
$this->drupalGet('/admin/reports/migration-messages');
$session->statusCodeEquals(200);
$session->pageTextContains('Failed to connect to your database server');
$session->pageTextContains('database connection configured for source plugin variable.');
foreach ($this->migrationIds as $migration_id) {
$session->pageTextContains($migration_id);
}
// Create a source database connection.
$this->createMigrationConnection();
$this->sourceDatabase = Database::getConnection('default', 'migrate_drupal_ui');
$this->createSourceTables();
// Now, test with a source database connection and with message tables.
$this->drupalGet('/admin/reports/migration-messages');
$session->statusCodeEquals(200);
$session->pageTextNotContains('Failed to connect to your database server');
foreach ($this->migrationIds as $migration_id) {
$session->pageTextContains($migration_id);
}
}
/**
* Tests the detail pages for migrate messages.
*
* Tests the detail page with the following scenarios;
* - No source database connection or message tables with a valid and an
* invalid migration.
* - A source database connection with message tables with a valid and an
* invalid migration.
* - A source database connection with message tables and a source plugin
* that does not have a description for a source ID in the values returned
* from fields().
*/
public function testDetail(): void {
$session = $this->assertSession();
// Details page with invalid migration.
$this->drupalGet('/admin/reports/migration-messages/invalid');
$session->statusCodeEquals(404);
$session->pageTextContains('Failed to connect to your database server');
// Details page with valid migration.
$this->drupalGet('/admin/reports/migration-messages/d7_menu');
$session->statusCodeEquals(404);
$session->pageTextNotContains('Failed to connect to your database server');
// Create map and message tables.
$this->createMigrateTables($this->migrationIds);
$not_available_text = "When there is an error processing a row, the migration system saves the error message but not the source ID(s) of the row. That is why some messages in this table have 'Not available' in the source ID column(s).";
// Test overview without a source database connection and with message
// tables.
$this->drupalGet('/admin/reports/migration-messages');
$session->statusCodeEquals(200);
foreach ($this->migrationIds as $migration_id) {
$session->pageTextContains($migration_id);
}
// Test details page for each migration.
foreach ($this->migrationIds as $migration_id) {
$this->drupalGet("/admin/reports/migration-messages/$migration_id");
$session->statusCodeEquals(200);
$session->pageTextNotContains('No database connection configured for source plugin');
$session->pageTextContains($migration_id);
if ($migration_id == 'd7_menu') {
// Confirm the descriptions from fields() are displayed.
$session->pageTextContains('MENU NAME. PRIMARY KEY');
$session->pageTextContains('Not available');
$session->pageTextContains($not_available_text);
}
}
// Create a source database connection.
$this->createMigrationConnection();
$this->sourceDatabase = Database::getConnection('default', 'migrate_drupal_ui');
$this->createSourceTables();
// Now, test with a source database connect and with message tables.
// Details page exists for each migration.
foreach ($this->migrationIds as $migration_id) {
$this->drupalGet("/admin/reports/migration-messages/$migration_id");
$session->statusCodeEquals(200);
$session->pageTextNotContains('No database connection configured for source plugin');
$session->pageTextContains($migration_id);
// Confirm the descriptions from fields() are displayed using d7_menu.
if ($migration_id == 'd7_menu') {
$session->pageTextContains('MENU NAME. PRIMARY KEY');
$session->pageTextContains('Not available');
$session->pageTextContains($not_available_text);
}
// Confirm the descriptions from fields() are displayed using
// d7_menu_test, which has a source plugin that is missing the
// 'menu_name' entry in fields().
if ($migration_id == 'd7_menu_test') {
$session->pageTextContains('MENU_NAME');
$session->pageTextContains('Not available');
$session->pageTextContains($not_available_text);
}
}
// Details page for a migration without a map table.
$this->database->schema()->dropTable('migrate_map_d7_menu');
$this->drupalGet('/admin/reports/migration-messages/d7_menu');
$session->statusCodeEquals(404);
// Details page for a migration with a map table but no message table.
$this->database->schema()->dropTable('migrate_message_d7_menu_links');
$this->drupalGet('/admin/reports/migration-messages/d7_menu_links');
$session->statusCodeEquals(200);
$session->pageTextContains('The message table is missing for this migration.');
}
/**
* Creates map and message tables for testing.
*
* @see \Drupal\migrate\Plugin\migrate\id_map\Sql::ensureTables
*/
protected function createMigrateTables(array $migration_ids): void {
foreach ($migration_ids as $migration_id) {
$map_table_name = "migrate_map_$migration_id";
$message_table_name = "migrate_message_$migration_id";
if (!$this->database->schema()->tableExists($map_table_name)) {
$fields = [];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
];
$fields['sourceid1'] = [
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
];
$fields['destid1'] = [
'type' => 'varchar',
'length' => '255',
'not null' => FALSE,
];
$fields['source_row_status'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::STATUS_IMPORTED,
];
$fields['rollback_action'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::ROLLBACK_DELETE,
];
$fields['last_imported'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
];
$fields['hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
];
$schema = [
'description' => '',
'fields' => $fields,
'primary key' => ['source_ids_hash'],
];
$this->database->schema()->createTable($map_table_name, $schema);
$rows = [
[
'source_ids_hash' => '37c655d',
'sourceid1' => 'navigation',
'destid1' => 'tools',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '3a34190',
'sourceid1' => 'menu-fixedlang',
'destid1' => 'menu-fixedlang',
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '3e51f67',
'sourceid1' => 'management',
'destid1' => 'admin',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '94a5caa',
'sourceid1' => 'user-menu',
'destid1' => 'account',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => 'c0efbcca',
'sourceid1' => 'main-menu',
'destid1' => 'main',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => 'f64cb72f',
'sourceid1' => 'menu-test-menu',
'destid1' => 'menu-test-menu',
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
],
];
foreach ($rows as $row) {
$this->database->insert($map_table_name)->fields($row)->execute();
}
}
if (!$this->database->schema()->tableExists($message_table_name)) {
$fields = [];
$fields['msgid'] = [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
];
$fields['level'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
];
$fields['message'] = [
'type' => 'text',
'size' => 'medium',
'not null' => TRUE,
];
$schema = [
'description' => '',
'fields' => $fields,
'primary key' => ['msgid'],
];
$this->database->schema()->createTable($message_table_name, $schema);
$rows = [
[
'msgid' => '1',
'source_ids_hash' => '28cfb3d1',
'level' => '1',
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '2',
'source_ids_hash' => '28cfb3d1',
'level' => '1',
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '3',
'source_ids_hash' => '05914d93',
'level' => '1',
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '4',
'source_ids_hash' => '05914d93',
'level' => '1',
'message' => 'Config entities can not be stubbed.',
],
];
foreach ($rows as $row) {
$this->database->insert($message_table_name)->fields($row)->execute();
}
}
}
}
/**
* Create source tables.
*/
protected function createSourceTables(): void {
$this->sourceDatabase->schema()->createTable('menu_custom', [
'fields' => [
'menu_name' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '32',
'default' => '',
],
'title' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
],
'description' => [
'type' => 'text',
'not null' => FALSE,
'size' => 'normal',
],
],
'primary key' => [
'menu_name',
],
'mysql_character_set' => 'utf8',
]);
$this->sourceDatabase->schema()->createTable('profile_values', [
'fields' => [
'fid' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
'unsigned' => TRUE,
],
'uid' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
'unsigned' => TRUE,
],
'value' => [
'type' => 'text',
'not null' => FALSE,
'size' => 'normal',
],
],
'primary key' => [
'fid',
'uid',
],
'mysql_character_set' => 'utf8',
]);
$this->sourceDatabase->schema()->createTable('profile_fields', [
'fields' => [
'fid' => [
'type' => 'serial',
'not null' => TRUE,
'size' => 'normal',
],
'title' => [
'type' => 'varchar',
'not null' => FALSE,
'length' => '255',
],
'name' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '128',
'default' => '',
],
'explanation' => [
'type' => 'text',
'not null' => FALSE,
'size' => 'normal',
],
'category' => [
'type' => 'varchar',
'not null' => FALSE,
'length' => '255',
],
'page' => [
'type' => 'varchar',
'not null' => FALSE,
'length' => '255',
],
'type' => [
'type' => 'varchar',
'not null' => FALSE,
'length' => '128',
],
'weight' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
],
'required' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
],
'register' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
],
'visibility' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
],
'autocomplete' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
],
'options' => [
'type' => 'text',
'not null' => FALSE,
'size' => 'normal',
],
],
'primary key' => [
'fid',
],
'mysql_character_set' => 'utf8',
]);
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath(): string {
return '';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts(): array {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths(): array {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths(): array {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental(): array {
return [];
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
/**
* Provides a base class for testing a complete upgrade via the UI.
*/
abstract class MigrateUpgradeExecuteTestBase extends MigrateUpgradeTestBase {
use CreateTestContentEntitiesTrait;
/**
* Indicates if the watchdog logs should be output.
*
* @var bool
*/
protected bool $outputLogs = FALSE;
/**
* The admin username after the migration.
*
* @var string
*/
protected string $migratedAdminUserName = 'admin';
/**
* The number of expected logged errors of type migrate_drupal_ui.
*
* @var int
*/
protected int $expectedLoggedErrors = 0;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create content.
$this->createContent();
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
if ($this->outputLogs) {
$this->outputLogs($this->migratedAdminUserName);
}
parent::tearDown();
}
/**
* Executes an upgrade and then an incremental upgrade.
*/
public function doUpgradeAndIncremental() {
// Start the upgrade process.
$this->submitCredentialForm();
$session = $this->assertSession();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Test the review form.
$this->assertReviewForm();
$this->useTestMailCollector();
$this->submitForm([], 'Perform upgrade');
$this->assertUpgrade($this->getEntityCounts());
// Test incremental migration.
$this->createContentPostUpgrade();
$this->drupalGet('/upgrade');
$session->pageTextContains("An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal $this->destinationSiteVersion. Rollbacks are not yet supported through the user interface.");
$this->submitForm([], 'Import new configuration and content from old site');
$this->submitForm($this->edits, 'Review upgrade');
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Run the incremental migration and check the results.
$this->submitForm([], 'Perform upgrade');
$this->assertUpgrade($this->getEntityCountsIncremental());
}
/**
* Helper to set the test mail collector in settings.php.
*/
public function useTestMailCollector() {
// Set up an override.
$settings['config']['system.mail']['interface']['default'] = (object) [
'value' => 'test_mail_collector',
'required' => TRUE,
];
$settings['config']['system.mail']['mailer_dsn']['scheme'] = (object) [
'value' => 'null',
'required' => TRUE,
];
$settings['config']['system.mail']['mailer_dsn']['host'] = (object) [
'value' => 'null',
'required' => TRUE,
];
$this->writeSettings($settings);
}
/**
* Checks the number of the specified entity's revisions.
*
* Revision translations are excluded.
*
* @param string $content_entity_type_id
* The entity type ID of the content entity, e.g. 'node', 'media',
* 'block_content'.
* @param int $expected_revision_count
* The expected number of the revisions.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function assertEntityRevisionsCount(string $content_entity_type_id, int $expected_revision_count) {
$entity_storage = \Drupal::entityTypeManager()->getStorage($content_entity_type_id);
assert($entity_storage instanceof ContentEntityStorageInterface);
$revision_ids = $entity_storage
->getQuery()
->allRevisions()
->accessCheck(FALSE)
->execute();
$this->assertCount(
$expected_revision_count,
$revision_ids,
sprintf(
"The number of %s revisions is different than expected",
$content_entity_type_id
)
);
}
/**
* Asserts log errors.
*/
public function assertLogError(): void {
$db = \Drupal::service('database');
$num_errors = $db->select('watchdog', 'w')
->fields('w')
->condition('type', 'migrate_drupal_ui')
->condition('severity', RfcLogLevel::ERROR)
->countQuery()
->execute()
->fetchField();
$this->assertSame($this->expectedLoggedErrors, (int) $num_errors);
}
/**
* Preserve the logs pages.
*/
public function outputLogs(string $username): void {
// Ensure user 1 is accessing the admin log. Change the username because
// the migration changes the username of user 1 but not the password.
if (\Drupal::currentUser()->id() != 1) {
$this->rootUser->name = $username;
$this->drupalLogin($this->rootUser);
}
$this->drupalGet('/admin/reports/dblog');
while ($next_link = $this->getSession()->getPage()->findLink('Next page')) {
$next_link->click();
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\migrate_drupal\MigrationConfigurationTrait;
use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\WebAssert;
/**
* Tests the flow of the Migrate Drupal UI form.
*
* @group migrate_drupal_ui
*/
class MigrateUpgradeFormStepsTest extends BrowserTestBase {
use MigrationConfigurationTrait;
use CreateTestContentEntitiesTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Log in as user 1. Migrations in the UI can only be performed as user 1.
$this->drupalLogin($this->rootUser);
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* Tests the flow of the Migrate Drupal UI form.
*
* The Migrate Drupal UI uses several forms to guide you through the upgrade
* process. The forms displayed depend on if this is an incremental migration
* or if there are potential ID conflicts. The forms are to be displayed in
* this order; Overview or Incremental, if a migration has already been run
* then Credential, Id conflict, if conflicts are detected, and lastly Review.
*/
public function testMigrateUpgradeReviewPage(): void {
/** @var \Drupal\Core\TempStore\PrivateTempStore $store */
$store = \Drupal::service('tempstore.private')->get('migrate_drupal_ui');
$state = \Drupal::service('state');
// Test that when data required by a form is missing that the correct first
// form is displayed. The first form for an initial migration is the
// Overview form and for an incremental migration it is the Incremental
// form.
$session = $this->assertSession();
// Get the current major version.
[$destination_site_version] = explode('.', \Drupal::VERSION, 2);
$expected['initial'] = "Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal $destination_site_version.";
$expected['incremental'] = "An upgrade has already been performed on this site.";
foreach (['/upgrade', '/upgrade/incremental'] as $expected) {
if ($expected === '/upgrade/incremental') {
// Set a performed time to signify an incremental migration. The time
// value is a UNIX timestamp.
$state->set('migrate_drupal_ui.performed', 1);
}
// Test that an invalid step to any form goes to the correct first form.
$store->set('step', 'foo');
$this->assertFirstForm($session, $expected);
// Test that an undefined step to any form goes to the correct first form.
$store->delete('step');
$this->assertFirstForm($session, $expected);
// For forms that require data from the private store, test that when that
// data is missing the correct first page is displayed.
// The Id conflict form requires the migrations array.
$store->delete('migrations');
$store->set('step', 'idconflict');
$this->drupalGet('/upgrade/idconflict');
$session->addressEquals($expected);
// The Review form requires version, migrations and system_data. Test
// three times with only one of the variables missing.
$store->delete('version');
$store->set('migrations', ['foo', 'bar']);
$store->set('system_data', ['bar', 'foo']);
$store->set('step', 'review');
$this->drupalGet('/upgrade/review');
$session->addressEquals($expected);
$store->set('version', '6');
$store->delete('migrations');
$store->set('system_data', ['bar', 'foo']);
$store->set('step', 'review');
$this->drupalGet('/upgrade/review');
$session->addressEquals($expected);
$store->set('version', '6');
$store->set('migrations', ['foo', 'bar']);
$store->delete('system_data');
$store->set('step', 'review');
$this->drupalGet('/upgrade/review');
$session->addressEquals($expected);
}
// Test that the credential form is displayed for incremental migrations.
$store->set('step', 'overview');
$this->drupalGet('/upgrade');
$session->pageTextContains("An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal $destination_site_version. Rollbacks are not yet supported through the user interface.");
$this->submitForm([], 'Import new configuration and content from old site');
$session->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.');
}
/**
* Helper to test that a path goes to the Overview form.
*
* @param \Drupal\Tests\WebAssert $session
* The WebAssert object.
* @param string $expected
* The expected response text.
*
* @internal
*/
protected function assertFirstForm(WebAssert $session, string $expected): void {
$paths = [
'',
'/incremental',
'/credentials',
'/idconflict',
'/review',
];
foreach ($paths as $path) {
$this->drupalGet('/upgrade' . $path);
$session->addressEquals($expected);
}
}
}

View File

@@ -0,0 +1,374 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Core\Database\Database;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate_drupal\MigrationConfigurationTrait;
use Drupal\user\Entity\User;
use Drupal\Tests\BrowserTestBase;
/**
* Provides a base class for testing migration upgrades in the UI.
*/
abstract class MigrateUpgradeTestBase extends BrowserTestBase {
use MigrationConfigurationTrait;
/**
* Use the Standard profile to test help implementations of many core modules.
*
* @var string
*/
protected $profile = 'standard';
/**
* The source database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $sourceDatabase;
/**
* The destination site major version.
*
* @var string
*/
protected $destinationSiteVersion;
/**
* Input data for the credential form.
*
* @var array
*/
protected $edits;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createMigrationConnection();
$this->sourceDatabase = Database::getConnection('default', 'migrate_drupal_ui');
// Get the current major version.
[$this->destinationSiteVersion] = explode('.', \Drupal::VERSION, 2);
// Log in as user 1. Migrations in the UI can only be performed as user 1.
$this->drupalLogin($this->rootUser);
}
/**
* Navigates to the credential form and submits valid credentials.
*/
public function submitCredentialForm() {
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
// Get valid credentials.
$this->edits = $this->translatePostValues($this->getCredentials());
// When the Credential form is submitted the migrate map tables are created.
$this->submitForm($this->edits, 'Review upgrade');
}
/**
* Loads a database fixture into the source database connection.
*
* @param string $path
* Path to the dump file.
*/
protected function loadFixture($path) {
$default_db = Database::getConnection()->getKey();
Database::setActiveConnection($this->sourceDatabase->getKey());
if (str_ends_with($path, '.gz')) {
$path = 'compress.zlib://' . $path;
}
require $path;
Database::setActiveConnection($default_db);
}
/**
* Changes the database connection to the prefixed one.
*
* @todo Remove when we don't use global. https://www.drupal.org/node/2552791
*/
protected function createMigrationConnection() {
$connection_info = Database::getConnectionInfo('default')['default'];
if ($connection_info['driver'] === 'sqlite') {
// Create database file in the test site's public file directory so that
// \Drupal\Tests\BrowserTestBase::cleanupEnvironment() will delete this
// once the test is complete.
$file = $this->publicFilesDirectory . '/' . $this->testId . '-migrate.db.sqlite';
touch($file);
$connection_info['database'] = $file;
$connection_info['prefix'] = '';
}
else {
$prefix = $connection_info['prefix'];
// Test databases use fixed length prefixes. Create a new prefix for the
// source database. Adding to the end of the prefix ensures that
// \Drupal\Tests\BrowserTestBase::cleanupEnvironment() will remove the
// additional tables.
$connection_info['prefix'] = $prefix . '0';
}
Database::addConnectionInfo('migrate_drupal_ui', 'default', $connection_info);
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
Database::removeConnection('migrate_drupal_ui');
parent::tearDown();
}
/**
* Gets the source base path for the concrete test.
*
* @return string
* The source base path.
*/
abstract protected function getSourceBasePath();
/**
* Gets the expected number of entities per entity type after migration.
*
* @return int[]
* An array of expected counts keyed by entity type ID.
*/
abstract protected function getEntityCounts();
/**
* Gets the available upgrade paths.
*
* @return string[]
* An array of available upgrade paths.
*/
abstract protected function getAvailablePaths();
/**
* Gets the missing upgrade paths.
*
* @return string[]
* An array of missing upgrade paths.
*/
abstract protected function getMissingPaths();
/**
* Gets expected number of entities per entity after incremental migration.
*
* @return int[]
* An array of expected counts keyed by entity type ID.
*/
abstract protected function getEntityCountsIncremental();
/**
* Helper method that asserts text on the ID conflict form.
*
* @param array $entity_types
* An array of entity types.
*
* @throws \Behat\Mink\Exception\ResponseTextException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function assertIdConflictForm(array $entity_types) {
$session = $this->assertSession();
/** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
$session->pageTextContains('WARNING: Content may be overwritten on your new site.');
$session->pageTextContains('There is conflicting content of these types:');
$this->assertNotEmpty($entity_types);
foreach ($entity_types as $entity_type) {
$label = $entity_type_manager->getDefinition($entity_type)->getPluralLabel();
$session->pageTextContains($label);
}
$session->pageTextContainsOnce('content items');
$session->pageTextContains('Check whether there is translated content of these types:');
}
/**
* Helper to assert content on the Review form.
*
* @param array|null $available_paths
* An array of modules that will be upgraded. Defaults to
* $this->getAvailablePaths().
* @param array|null $missing_paths
* An array of modules that will not be upgraded. Defaults to
* $this->getMissingPaths().
*
* @throws \Behat\Mink\Exception\ExpectationException
*/
protected function assertReviewForm(?array $available_paths = NULL, ?array $missing_paths = NULL) {
$session = $this->assertSession();
$session->pageTextContains('What will be upgraded?');
$available_paths = $available_paths ?? $this->getAvailablePaths();
$missing_paths = $missing_paths ?? $this->getMissingPaths();
// Test the available migration paths.
foreach ($available_paths as $available) {
$session->elementExists('xpath', "//td[contains(@class, 'checked') and text() = '$available']");
$session->elementNotExists('xpath', "//td[contains(@class, 'error') and text() = '$available']");
}
// Test the missing migration paths.
foreach ($missing_paths as $missing) {
$session->elementExists('xpath', "//td[contains(@class, 'error') and text() = '$missing']");
$session->elementNotExists('xpath', "//td[contains(@class, 'checked') and text() = '$missing']");
}
// Test the total count of missing and available paths.
$session->elementsCount('xpath', "//td[contains(@class, 'upgrade-analysis-report__status-icon--error')]", count($missing_paths));
$session->elementsCount('xpath', "//td[contains(@class, 'upgrade-analysis-report__status-icon--checked')]", count($available_paths));
}
/**
* Asserts the upgrade completed successfully.
*
* @param array $entity_counts
* An array of entity count, where the key is the entity type and the value
* is the number of the entities that should exist post migration.
*
* @throws \Behat\Mink\Exception\ExpectationException
*/
protected function assertUpgrade(array $entity_counts) {
$session = $this->assertSession();
$session->pageTextContains(t('Congratulations, you upgraded Drupal!'));
// Assert the count of entities after the upgrade. First, reset all the
// statics after migration to ensure entities are loadable.
$this->resetAll();
// Check that the expected number of entities is the same as the actual
// number of entities.
$entity_definitions = array_keys(\Drupal::entityTypeManager()->getDefinitions());
ksort($entity_counts);
$expected_count_keys = array_keys($entity_counts);
sort($entity_definitions);
$this->assertSame($expected_count_keys, $entity_definitions);
// Assert the correct number of entities exists.
$actual_entity_counts = [];
foreach ($entity_definitions as $entity_type) {
$actual_entity_counts[$entity_type] = (int) \Drupal::entityQuery($entity_type)->accessCheck(FALSE)->count()->execute();
}
$this->assertSame($entity_counts, $actual_entity_counts);
$plugin_manager = \Drupal::service('plugin.manager.migration');
$version = $this->getLegacyDrupalVersion($this->sourceDatabase);
/** @var \Drupal\migrate\Plugin\Migration[] $all_migrations */
$all_migrations = $plugin_manager->createInstancesByTag('Drupal ' . $version);
foreach ($all_migrations as $migration) {
$id_map = $migration->getIdMap();
foreach ($id_map as $source_id => $map) {
// Convert $source_id into a keyless array so that
// \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as
// expected.
$source_id_values = array_values(unserialize($source_id));
$row = $id_map->getRowBySource($source_id_values);
$destination = serialize($id_map->currentDestination());
$message = "Migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status'];
// A completed migration should have maps with
// MigrateIdMapInterface::STATUS_IGNORED or
// MigrateIdMapInterface::STATUS_IMPORTED.
$this->assertNotSame(MigrateIdMapInterface::STATUS_FAILED, $row['source_row_status'], $message);
$this->assertNotSame(MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $row['source_row_status'], $message);
}
}
}
/**
* Creates an array of credentials for the Credential form.
*
* Before submitting to the Credential form the array must be processed by
* BrowserTestBase::translatePostValues() before submitting.
*
* @return array
* An array of values suitable for BrowserTestBase::translatePostValues().
*
* @see \Drupal\migrate_drupal_ui\Form\CredentialForm
*/
protected function getCredentials() {
$connection_options = $this->sourceDatabase->getConnectionOptions();
$version = $this->getLegacyDrupalVersion($this->sourceDatabase);
$driver = $connection_options['driver'];
// Use the driver connection form to get the correct options out of the
// database settings. This supports all of the databases we test against.
$drivers = Database::getDriverList()->getInstallableList();
$form = $drivers[$driver]->getInstallTasks()->getFormOptions($connection_options);
$connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']);
// Remove isolation_level since that option is not configurable in the UI.
unset($connection_options['isolation_level']);
$edit = [
$driver => $connection_options,
'source_private_file_path' => $this->getSourceBasePath(),
'version' => $version,
];
if ($version == 6) {
$edit['d6_source_base_path'] = $this->getSourceBasePath();
}
else {
$edit['source_base_path'] = $this->getSourceBasePath();
$edit['source_private_file_path'] = $this->getSourcePrivateBasePath();
}
if (count($drivers) !== 1) {
$edit['driver'] = $driver;
}
return $edit;
}
/**
* Asserts that a migrated user can login.
*/
public function assertUserLogIn($uid, $pass) {
$user = User::load($uid);
$user->passRaw = $pass;
$this->drupalLogin($user);
}
/**
* Provides the source base path for private files for the credential form.
*
* @return string|null
* The source base path.
*/
protected function getSourcePrivateBasePath() {
return NULL;
}
/**
* Checks public and private files are copied but not temporary files.
*/
protected function assertFileMigrations() {
$fs = \Drupal::service('file_system');
$files = $this->getManagedFiles();
foreach ($files as $file) {
preg_match('/^(private|public|temporary):/', $file['uri'], $matches);
$scheme = $matches[1];
$filepath = $fs->realpath($file['uri']);
if ($scheme === 'temporary') {
$this->assertFileDoesNotExist($filepath);
}
else {
$this->assertFileExists($filepath);
}
}
}
/**
* Confirm emails were sent.
*/
protected function assertEmailsSent() {
// There should be one user activation email.
$captured_emails = \Drupal::state()->get('system.test_mail_collector', []);
$this->assertCount(1, $captured_emails);
$this->assertEquals('user_status_activated', $captured_emails[0]['id']);
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
/**
* Provides a base class for testing the review step of the Upgrade form.
*
* When using this test class, enable translation modules.
*/
abstract class MultilingualReviewPageTestBase extends MigrateUpgradeTestBase {
use CreateTestContentEntitiesTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_drupal_ui'];
/**
* Tests the migrate upgrade review form.
*
* The upgrade review form displays a list of modules that will be upgraded
* and a list of modules that will not be upgraded. This test is to ensure
* that the review page works correctly for all contributed Drupal 6 and
* Drupal 7 modules that have moved to core, e.g. Views, and for modules that
* were in Drupal 6 or Drupal 7 core but are not in Drupal 8 core, e.g.
* Overlay.
*
* To do this all modules in the source fixtures are enabled, except test and
* example modules. This means that we can test that the modules that do not
* need any migrations, such as Overlay, since there will be no available
* migrations which declare those modules as their source_module. It is
* assumed that the test fixtures include all modules that have moved to or
* dropped from core.
*
* The upgrade review form will also display errors for each migration that
* does not have a source_module definition. That function is not tested here.
*
* @see \Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase
*/
public function testMigrateUpgradeReviewPage(): void {
$this->prepare();
// Start the upgrade process.
$this->submitCredentialForm();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
// Test the upgrade paths.
$this->assertReviewForm();
// Check there are no errors when a module does not have any migrations and
// does not need any. Test with a module that is in both Drupal 6 and
// Drupal 7 core.
$module = 'help';
$module_name = 'Help';
$query = $this->sourceDatabase->delete('system');
$query->condition('type', 'module');
$query->condition('name', $module);
$query->execute();
// Start the upgrade process.
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
$this->submitForm($this->edits, 'Review upgrade');
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
// Test the upgrade paths. First remove the module from the available paths
// list.
$available_paths = $this->getAvailablePaths();
$available_paths = array_diff($available_paths, [$module_name]);
$this->assertReviewForm($available_paths);
}
/**
* Performs preparation for the form tests.
*
* This is not done in setup because setup executes before the source database
* is loaded.
*/
public function prepare() {
// Enable all modules in the source except test and example modules, but
// include simpletest.
/** @var \Drupal\Core\Database\Query\SelectInterface $update */
$update = $this->sourceDatabase->update('system')
->fields(['status' => 1])
->condition('type', 'module');
$and = $update->andConditionGroup()
->condition('name', '%test%', 'NOT LIKE')
->condition('name', '%example%', 'NOT LIKE');
$conditions = $update->orConditionGroup();
$conditions->condition($and);
$conditions->condition('name', 'simpletest');
$update->condition($conditions);
$update->execute();
// Create entries for D8 test modules.
$insert = $this->sourceDatabase->insert('system')
->fields([
'filename' => 'migrate_status_active_test',
'name' => 'migrate_status_active_test',
'type' => 'module',
'status' => 1,
]);
$insert->execute();
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
/**
* Tests the upgrade review form without translations.
*/
abstract class NoMultilingualReviewPageTestBase extends MultilingualReviewPageTestBase {
/**
* Tests the review page when content_translation is enabled.
*/
public function testMigrateUpgradeReviewPage(): void {
$this->prepare();
// Start the upgrade process.
$this->submitCredentialForm();
$session = $this->assertSession();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Test the upgrade paths.
$this->assertReviewForm();
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional;
/**
* Tests that a missing source provider error message is displayed.
*
* @group migrate_drupal_ui
* @group #slow
*/
class SourceProviderTest extends MigrateUpgradeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate_drupal_ui',
// Will generate an error for a missing source module.
'migration_provider_test',
];
/**
* Test missing source provider.
*
* @dataProvider providerSourceProvider
*/
public function testSourceProvider($path_to_database): void {
$this->loadFixture($this->getModulePath('migrate_drupal') . $path_to_database);
$session = $this->assertSession();
// Start the upgrade process.
$this->submitCredentialForm();
// Ensure we get errors about missing modules.
$session->pageTextContains('Resolve all issues below to continue the upgrade.');
$session->pageTextContains('The no_source_module plugin must define the source_module property.');
// Uninstall the module causing the missing module error messages.
$this->container->get('module_installer')
->uninstall(['migration_provider_test'], TRUE);
// Restart the upgrade process and test there is no source_module error.
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
$this->submitForm($this->edits, 'Review upgrade');
// Ensure there are no errors about missing modules from the test module.
$session->pageTextNotContains('Source module not found for migration_provider_no_annotation.');
$session->pageTextNotContains('Source module not found for migration_provider_test.');
// Ensure there are no errors about any other missing migration providers.
$session->pageTextNotContains('module not found');
}
/**
* Data provider for testSourceProvider.
*/
public static function providerSourceProvider() {
return [
[
'path_to_database' => '/tests/fixtures/drupal6.php',
],
[
'path_to_database' => '/tests/fixtures/drupal7.php',
],
];
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return '';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d6;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
/**
* Tests Drupal 6 Id Conflict page.
*
* @group migrate_drupal_ui
*/
class IdConflictTest extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_translation',
'content_translation',
'language',
'migrate_drupal_ui',
'telephone',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal6.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
/**
* Tests ID Conflict form.
*/
public function testIdConflictForm(): void {
// Start the upgrade process.
$this->submitCredentialForm();
$entity_types = [
'block_content',
'menu_link_content',
'file',
'taxonomy_term',
'user',
'comment',
'node',
];
$this->assertIdConflictForm($entity_types);
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d6;
use Drupal\Tests\migrate_drupal_ui\Functional\MultilingualReviewPageTestBase;
// cspell:ignore multigroup nodeaccess
/**
* Tests migrate upgrade review page for Drupal 6.
*
* Tests with translation modules enabled.
*
* @group migrate_drupal_6
* @group migrate_drupal_ui
*/
class MultilingualReviewPageTest extends MultilingualReviewPageTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'datetime_range',
'language',
'content_translation',
'config_translation',
'telephone',
'syslog',
'update',
// Test migrations states.
'migrate_state_finished_test',
'migrate_state_not_finished_test',
// Test missing migrate_drupal.yml.
'migrate_state_no_upgrade_path',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal6.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block translation',
'Blog',
'Blog API',
'CCK translation',
'Calendar Signup',
'Comment',
'Contact',
'Content',
'Content Copy',
'Content Multigroup',
'Content Permissions',
'Content translation',
'Content type translation',
'Database logging',
'Date',
'Date API',
'Date Locale',
'Date PHP4',
'Date Picker',
'Date Popup',
'Date Repeat API',
'Date Timezone',
'Date Tools',
'Dynamic display block',
'Email',
'Event',
'Fieldgroup',
'FileField',
'FileField Meta',
'Filter',
'Help',
'ImageAPI',
'ImageAPI GD2',
'ImageAPI ImageMagick',
'ImageCache',
'ImageCache UI',
'ImageField',
'Internationalization',
'Link',
'Locale',
'Menu',
'Menu translation',
'Node',
'Node Reference',
'Node Reference URL Widget',
'Nodeaccess',
'Number',
'OpenID',
'PHP filter',
'Path',
'Phone - CCK',
'Ping',
'Poll',
'Poll aggregate',
'Profile',
'Profile translation',
'Search',
'String translation',
'Synchronize translations',
'Syslog',
'System',
'Taxonomy translation',
'Taxonomy',
'Text',
'Throttle',
// @todo Remove Tracker in https://www.drupal.org/project/drupal/issues/3261452
'Tracker',
'Trigger',
'Update status',
'Upload',
'User',
'User Reference',
'Variable API',
'Variable admin',
'Views UI',
'Views exporter',
'jQuery UI',
];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
'Aggregator',
'Book',
// Block is set not_finished in migrate_state_not_finished_test.
'Block',
'Color',
'Devel',
'Devel generate',
'Devel node access',
'Forum',
'Statistics',
// Option Widgets is set not_finished in migrate_state_not_finished_test.
'Option Widgets',
'Views',
'Views translation',
'migrate_status_active_test',
];
}
}

View File

@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d6;
use Drupal\Tests\migrate_drupal_ui\Functional\NoMultilingualReviewPageTestBase;
// cspell:ignore multigroup nodeaccess
/**
* Tests migrate upgrade review page for Drupal 6 without translations.
*
* Tests with the translation modules disabled.
*
* @group migrate_drupal_6
* @group migrate_drupal_ui
*/
class NoMultilingualReviewPageTest extends NoMultilingualReviewPageTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'datetime_range',
'language',
'telephone',
'syslog',
'update',
// Test migrations states.
'migrate_state_finished_test',
'migrate_state_not_finished_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal6.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Blog',
'Blog API',
'Calendar Signup',
'Comment',
'Contact',
'Content',
'Content Copy',
'Content Multigroup',
'Content Permissions',
'Content translation',
'Database logging',
'Date',
'Date API',
'Date Locale',
'Date PHP4',
'Date Picker',
'Date Popup',
'Date Repeat API',
'Date Timezone',
'Date Tools',
'Dynamic display block',
'Email',
'Event',
'Fieldgroup',
'FileField',
'FileField Meta',
'Filter',
'Help',
'ImageAPI',
'ImageAPI GD2',
'ImageAPI ImageMagick',
'ImageCache',
'ImageCache UI',
'ImageField',
'Link',
'Locale',
'Menu',
'Node',
'Nodeaccess',
'Node Reference',
'Node Reference URL Widget',
'Number',
'OpenID',
'PHP filter',
'Path',
'Phone - CCK',
'Ping',
'Poll',
'Profile',
'Search',
'Syslog',
'System',
'Taxonomy',
'Text',
'Throttle',
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
'Tracker',
'Trigger',
'Update status',
'Upload',
'User',
'User Reference',
'Variable API',
'Variable admin',
'Views UI',
'Views exporter',
'jQuery UI',
];
}
/**
* {@inheritdoc}
*/
protected function getIncompletePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
'Aggregator',
// Block is set not_finished in migrate_state_not_finished_test.
'Block',
'Block translation',
'Book',
'CCK translation',
'Color',
'Content type translation',
'Devel',
'Devel generate',
'Devel node access',
'Forum',
'Internationalization',
'Menu translation',
'migrate_status_active_test',
// Option Widgets is set not_finished in migrate_state_not_finished_test.
'Option Widgets',
'Poll aggregate',
'Profile translation',
'Statistics',
'String translation',
'Synchronize translations',
'Taxonomy translation',
'Views',
'Views translation',
];
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d6;
use Drupal\migrate_drupal\NodeMigrateType;
use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
/**
* Tests the classic node migration runs.
*
* The classic node migration will run and not the complete node migration
* when there is a pre-existing classic node migrate map table.
*
* @group migrate_drupal_ui
*/
class NodeClassicTest extends MigrateUpgradeExecuteTestBase {
use NodeMigrateTypeTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'language',
'content_translation',
'config_translation',
'migrate_drupal_ui',
'telephone',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal6.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
/**
* Tests node classic migration via the UI.
*/
public function testNodeClassicUpgrade(): void {
// Add a node classic migrate table to d8.
$this->makeNodeMigrateMapTable(NodeMigrateType::NODE_MIGRATE_TYPE_CLASSIC, '6');
// Start the upgrade process.
$this->submitCredentialForm();
// Confirm there are only classic node migration map tables. This shows
// that only the classic migration will run.
$results = $this->nodeMigrateMapTableCount('6');
$this->assertSame(14, $results['node']);
$this->assertSame(0, $results['node_complete']);
}
}

View File

@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d6;
use Drupal\node\Entity\Node;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
/**
* Tests Drupal 6 upgrade using the migrate UI.
*
* The test method is provided by the MigrateUpgradeTestBase class.
*
* @group migrate_drupal_ui
* @group #slow
*/
class Upgrade6Test extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_translation',
'content_translation',
'datetime_range',
'language',
'migrate_drupal_ui',
'telephone',
'update',
];
/**
* The entity storage for node.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// @todo remove in https://www.drupal.org/project/drupal/issues/3267040
// Delete the existing content made to test the ID Conflict form. Migrations
// are to be done on a site without content. The test of the ID Conflict
// form is being moved to its own issue which will remove the deletion
// of the created nodes.
// See https://www.drupal.org/project/drupal/issues/3087061.
$this->nodeStorage = $this->container->get('entity_type.manager')
->getStorage('node');
$this->nodeStorage->delete($this->nodeStorage->loadMultiple());
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal6.php');
$this->expectedLoggedErrors = 39;
// If saving the logs, then set the admin user.
if ($this->outputLogs) {
$this->migratedAdminUserName = 'admin';
}
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [
'block' => 37,
'block_content' => 2,
'block_content_type' => 1,
'comment' => 8,
// The 'standard' profile provides the 'comment' comment type, and the
// migration creates 12 comment types, one per node type.
'comment_type' => 14,
'contact_form' => 5,
'contact_message' => 0,
'configurable_language' => 5,
'editor' => 2,
'field_config' => 102,
'field_storage_config' => 71,
'file' => 7,
'filter_format' => 7,
'image_style' => 6,
'language_content_settings' => 15,
'node' => 18,
// The 'book' module provides the 'book' node type, and the migration
// creates 12 node types.
'node_type' => 13,
'search_page' => 3,
'shortcut' => 2,
'shortcut_set' => 1,
'action' => 30,
'menu' => 8,
'path_alias' => 8,
'taxonomy_term' => 15,
'taxonomy_vocabulary' => 7,
'user' => 7,
'user_role' => 7,
'menu_link_content' => 10,
'view' => 14,
'date_format' => 12,
'entity_form_display' => 29,
'entity_form_mode' => 1,
'entity_view_display' => 55,
'entity_view_mode' => 12,
'base_field_override' => 39,
];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
$counts = $this->getEntityCounts();
$counts['block_content'] = 3;
$counts['comment'] = 9;
$counts['file'] = 8;
$counts['menu_link_content'] = 11;
$counts['node'] = 19;
$counts['taxonomy_term'] = 16;
$counts['user'] = 8;
return $counts;
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block',
'Block translation',
'CCK translation',
'Comment',
'Contact',
'Content',
'Content translation',
'Content type translation',
'Date',
'Email',
'FileField',
'Filter',
'ImageCache',
'ImageField',
'Internationalization',
'Locale',
'Menu',
'Menu translation',
'Node',
'Node Reference',
'Node Reference URL Widget',
'Option Widgets',
'Path',
'Profile translation',
'Search',
'String translation',
'Synchronize translations',
'System',
'Taxonomy',
'Taxonomy translation',
'Text',
'Update status',
'Upload',
'User',
'User Reference',
// Include modules that do not have an upgrade path and are enabled in the
// source database'.
'Date API',
'Date Timezone',
'Event',
'ImageAPI',
'Number',
'PHP filter',
'Profile',
'Variable admin',
];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
'Aggregator',
'Book',
'Forum',
'Statistics',
];
}
/**
* Executes all steps of migrations upgrade.
*/
public function testUpgradeAndIncremental(): void {
// Perform upgrade followed by an incremental upgrade.
$this->doUpgradeAndIncremental();
// Ensure a migrated user can log in.
$this->assertUserLogIn(2, 'john.doe_pass');
$this->assertFollowUpMigrationResults();
$this->assertEntityRevisionsCount('node', 26);
$this->assertEmailsSent();
$this->assertLogError();
}
/**
* Tests that follow-up migrations have been run successfully.
*
* @internal
*/
protected function assertFollowUpMigrationResults(): void {
$node = Node::load(10);
$this->assertSame('12', $node->get('field_reference')->target_id);
$this->assertSame('12', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('12', $translation->get('field_reference')->target_id);
$this->assertSame('12', $translation->get('field_reference_2')->target_id);
$node = Node::load(12)->getTranslation('en');
$this->assertSame('10', $node->get('field_reference')->target_id);
$this->assertSame('10', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('10', $translation->get('field_reference')->target_id);
$this->assertSame('10', $translation->get('field_reference_2')->target_id);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d6;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\WorkflowInterface;
/**
* Tests Drupal 6 upgrade using the migrate UI with Content Moderation.
*
* @group migrate_drupal_ui
* @group #slow
*/
class Upgrade6TestWithContentModeration extends Upgrade6Test {
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_moderation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Set up a moderation flow.
$types = [
'story',
'test_planet',
'company',
'employee',
];
foreach ($types as $type) {
$this->drupalCreateContentType(['type' => $type]);
}
$editorial = Workflow::load('editorial');
assert($editorial instanceof WorkflowInterface);
$type_settings = $editorial->getTypePlugin()->getConfiguration();
$type_settings['default_moderation_state'] = 'published';
$type_settings['entity_types']['node'] = array_merge(
['page'],
$types
);
$type_plugin = $editorial->getTypePlugin();
$type_plugin->setConfiguration($type_settings);
$editorial->trustData()->save();
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
$entity_counts = parent::getEntityCounts() + [
'content_moderation_state' => 17,
'workflow' => 1,
];
$entity_counts['field_config'] = $entity_counts['field_config'] + 1;
$entity_counts['view'] = $entity_counts['view'] + 1;
return $entity_counts;
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
$entity_counts_incremental = parent::getEntityCountsIncremental();
$entity_counts_incremental['content_moderation_state'] = $entity_counts_incremental['content_moderation_state'] + 1;
return $entity_counts_incremental;
}
}

View File

@@ -0,0 +1 @@
<h1>Test HTML</h1>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

View File

@@ -0,0 +1 @@
<h1>Test HTML</h1>

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests that a double slash is not in d7_file file not found migrate messages.
*
* @group migrate_drupal_ui
*/
class DoubleSlashTest extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
'migrate_drupal_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture(\Drupal::service('extension.list.module')->getPath('migrate_drupal') . '/tests/fixtures/drupal7.php');
}
/**
* Executes all steps of migrations upgrade.
*/
public function testMigrateUpgradeExecute(): void {
// Change fid 1 to a filename that does not exist.
$this->sourceDatabase
->update('file_managed')
->condition('fid', 1)
->fields([
'filename' => 'foo.txt',
'uri' => 'public://foo.txt',
])
->execute();
// Get valid credentials.
$edits = $this->translatePostValues($this->getCredentials());
// Start the upgrade process.
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
$this->submitForm($edits, 'Review upgrade');
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$this->useTestMailCollector();
$this->submitForm([], 'Perform upgrade');
// Tests the migration log contains an error message.
$migration = $this->getMigrationPluginManager()->createInstance('d7_file');
$messages = $migration->getIdMap()->getMessages();
$count = 0;
foreach ($messages as $message) {
$count++;
$this->assertStringContainsString('/migrate_drupal_ui/tests/src/Functional/d7/files/sites/default/files/foo.txt', $message->message);
$this->assertSame(MigrationInterface::MESSAGE_ERROR, (int) $message->level);
}
$this->assertSame(1, $count);
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
}

View File

@@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\Core\Database\Database;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Tests\ExtensionListTestTrait;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeTestBase;
/**
* Tests the Drupal 7 public and private file migrations.
*
* To test file migrations both the public and private test source files are
* created in the temporary directory of the destination test site. Tests are
* done with the source files at the top level temporary directory and sub paths
* from that.
*
* @group migrate_drupal_ui
*/
class FilePathTest extends MigrateUpgradeTestBase {
use ExtensionListTestTrait;
/**
* {@inheritdoc}
*/
protected $profile = 'testing';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fs;
/**
* The base path to the source files on the destination site.
*
* @var string[]
*/
protected $localDirectory = [];
/**
* The file scheme variables in the source database.
*
* These are 'file_private_path', 'file_public_path', and
* 'file_temporary_path',
*
* @var string[]
*/
protected $sourceFileScheme = [];
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
'migrate',
'migrate_drupal',
'migrate_drupal_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fs = \Drupal::service('file_system');
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal7.php');
}
/**
* Executes all steps of migrations upgrade.
*
* @param string $file_private_path
* The source database file_private_path value.
* @param string $file_public_path
* The source database file_public_path value.
* @param string $file_temporary_path
* The source database file_temporary_path value.
* @param string $private
* The path to the source private files.
* @param string $public
* The path to the source public files.
* @param string $temporary
* The path to the source temporary files.
*
* @dataProvider providerTestFilePath
*/
public function testFilePath(string $file_private_path, string $file_public_path, string $file_temporary_path, string $private, string $public, string $temporary): void {
$this->sourceFileScheme['private'] = $file_private_path;
$this->sourceFileScheme['public'] = $file_public_path;
$this->sourceFileScheme['temporary'] = $file_temporary_path;
$this->localDirectory['private'] = $private;
$this->localDirectory['public'] = $public;
$this->localDirectory['temporary'] = $temporary;
// Create the source files.
$this->makeFiles();
// Set the source db variables.
$this->sourceDatabase->update('variable')
->fields(['value' => serialize($file_private_path)])
->condition('name', 'file_private_path')
->execute();
$this->sourceDatabase->update('variable')
->fields(['value' => serialize($file_public_path)])
->condition('name', 'file_public_path')
->execute();
$this->sourceDatabase->update('variable')
->fields(['value' => serialize($file_temporary_path)])
->condition('name', 'file_temporary_path')
->execute();
$connection_options = $this->sourceDatabase->getConnectionOptions();
$driver = $connection_options['driver'];
// Use the driver connection form to get the correct options out of the
// database settings. This supports all of the databases we test against.
$drivers = Database::getDriverList()->getInstallableList();
$form = $drivers[$driver]->getInstallTasks()->getFormOptions($connection_options);
$connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']);
// Remove isolation_level since that option is not configurable in the UI.
unset($connection_options['isolation_level']);
$edit = [
$driver => $connection_options,
'version' => '7',
];
if (count($drivers) !== 1) {
$edit['driver'] = $driver;
}
// Set the public and private base paths for the Credential Form.
$edit['source_private_file_path'] = $this->fs->realpath($this->getSourcePath('private'));
$edit['source_base_path'] = $this->fs->realpath($this->getSourcePath('public'));
$edits = $this->translatePostValues($edit);
// Start the upgrade.
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
$this->submitForm($edits, 'Review upgrade');
// The migrations are now in store - remove all but the file migrations.
$store = \Drupal::service('tempstore.private')->get('migrate_drupal_ui');
$migration_array = array_intersect_key(
$store->get('migrations'),
array_flip(['d7_file', 'd7_file_private'])
);
$store->set('migrations', $migration_array);
// Perform the migrations.
$this->submitForm([], 'Perform upgrade');
$this->resetAll();
$this->assertFileMigrations();
}
/**
* Data provider of test dates for file path test.
*
* @return string[][]
* An array of test data.
*/
public static function providerTestFilePath() {
return [
'All source base paths are at temporary' => [
'sites/default/private',
'sites/default/files',
'/tmp',
'',
'',
'',
],
'The private files are in a subdirectory' => [
'sites/default/private',
'sites/default/files',
'/tmp',
'abc',
'',
'',
],
' The public files are in a subdirectory' => [
'sites/default/private',
'sites/default/files',
'/tmp',
'',
'def',
'',
],
'The private, public and temporary files are in separate subdirectories' => [
'private',
'files',
'/tmp',
'abc',
'def',
'xyz',
],
];
}
/**
* Creates files for the test.
*
* The source files are written to a subdirectory of the temporary files
* directory of the test sites. The subdirectory path always ends with the
* path to the relevant scheme as set in the source variable table.
*
* For example:
* The source site files_managed table.
* uri: public://foo.txt
* filename: foo.txt
* The source site variable table.
* file_public_path: sites/default/files
* Local directory
* /bar
*
* The resulting directory is /bar/sites/default/files/foo.txt.
*/
protected function makeFiles() {
// Get file information from the source database.
foreach ($this->getManagedFiles() as $file) {
$this->assertSame(1, preg_match('/^(private|public|temporary):/', $file['uri'], $matches));
$scheme = $matches[1];
$path = $this->sourceFileScheme[$scheme] ?? '';
$filepath = implode('/', [
$this->getSourcePath($scheme),
$path,
$file['filename'],
]);
// Create the file.
$source_file = @fopen($filepath, 'w');
if (!$source_file) {
// If fopen didn't work, make sure there's a writable directory in
// place.
$dir = $this->fs->dirname($filepath);
$this->fs->prepareDirectory($dir, FileSystemInterface:: CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
// Let's try that fopen again.
$source_file = @fopen($filepath, 'w');
}
fwrite($source_file, '42');
}
}
/**
* Gets the source base path for the Credential form.
*
* @param string $scheme
* The file scheme.
*/
public function getSourcePath($scheme) {
$base_path = $this->localDirectory[$scheme] ?: '';
// Puts the source files in the site temp directory.
return $this->tempFilesDirectory . '/' . $base_path;
}
/**
* Gets the file data.
*
* @return string[][]
* Data from the source file_managed table.
*/
public function getManagedFiles() {
return [
[
'filename' => 'cube.jpeg',
'uri' => 'public://cube.jpeg',
],
[
'filename' => 'ds9.txt',
'uri' => 'public://ds9.txt',
],
[
'filename' => 'Babylon5.txt',
'uri' => 'private://Babylon5.txt',
],
[
'filename' => 'DeepSpaceNine.txt',
'uri' => 'temporary://DeepSpaceNine.txt',
],
];
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return '';
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
/**
* Tests Drupal 7 Id Conflict page.
*
* @group migrate_drupal_ui
*/
class IdConflictTest extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_translation',
'content_translation',
'language',
'migrate_drupal_ui',
'telephone',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal7.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
/**
* Tests ID Conflict form.
*/
public function testIdConflictForm(): void {
// Start the upgrade process.
$this->submitCredentialForm();
$entity_types = [
'block_content',
'menu_link_content',
'file',
'taxonomy_term',
'user',
'comment',
'node',
];
$this->assertIdConflictForm($entity_types);
}
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\Tests\migrate_drupal_ui\Functional\MultilingualReviewPageTestBase;
// cspell:ignore Filefield Flexslider Multiupload Imagefield rulesets
/**
* Tests migrate upgrade review page for Drupal 7.
*
* Tests with translation modules enabled.
*
* @group migrate_drupal_7
* @group migrate_drupal_ui
*/
class MultilingualReviewPageTest extends MultilingualReviewPageTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'datetime_range',
'language',
'content_translation',
'telephone',
'syslog',
'update',
// Test migrations states.
'migrate_state_finished_test',
'migrate_state_not_finished_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal7.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block languages',
'Blog',
'Bulk Export',
'Chaos tools',
'Chaos Tools (CTools) AJAX Example',
'Comment',
'Contact',
'Content translation',
'Contextual links',
'Custom content panes',
'Custom rulesets',
'Dashboard',
'Database logging',
'Date',
'Date API',
'Date All Day',
'Date Context',
'Date Migration',
'Date Popup',
'Date Repeat API',
'Date Repeat Field',
'Date Tools',
'Date Views',
'Email',
'Entity API',
'Entity Reference',
'Entity Translation',
'Entity feature module',
'Entity tokens',
'Field',
'Field SQL storage',
'Field UI',
'File',
'Filter',
'Help',
'Image',
'Internationalization',
'Link',
'List',
'Locale',
'Menu',
'Menu translation',
'Multiupload Filefield Widget',
'Multiupload Imagefield Widget',
'Node',
'Node Reference',
'Number',
'OpenID',
'Overlay',
'PHP filter',
'Page manager',
'Path',
'Phone',
'Poll',
'Profile',
'Search',
'Search embedded form',
'Shortcut',
'String translation',
'Stylizer',
'Synchronize translations',
'Syslog',
'System',
'Taxonomy translation',
'Taxonomy',
'Telephone',
'Term Depth access',
'Test search node tags',
'Test search type',
'Testing',
'Text',
'Title',
'Toolbar',
'Trigger',
'Update manager',
'User',
'User Reference',
'Views content panes',
'Views UI',
];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
// Action is set not_finished in migrate_state_not_finished_test.
'Aggregator',
// Block is set not_finished in migrate_state_not_finished_test.
'Block',
'Book',
'Breakpoints',
'Color',
'Contact translation',
'Entity Translation Menu',
'Entity Translation Upgrade',
'Field translation',
// Flexslider_picture is a sub module of Picture module. Only the
// styles from picture are migrated.
'FlexSlider Picture',
'Forum',
'Multilingual content',
'Multilingual forum',
'Multilingual select',
// Options is set not_finished in migrate_state_not_finished_test.
'Options',
'Path translation',
'Picture',
'RDF',
'References',
'References UUID',
'Statistics',
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
'Tracker',
'Translation redirect',
'Translation sets',
'User mail translation',
'Variable',
'Variable admin',
'Variable realm',
'Variable store',
'Variable translation',
'Variable views',
'Views',
'migrate_status_active_test',
];
}
}

View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\Tests\migrate_drupal_ui\Functional\NoMultilingualReviewPageTestBase;
// cspell:ignore Filefield Multiupload Imagefield rulesets
/**
* Tests Drupal 7 upgrade without translations.
*
* The test method is provided by the MigrateUpgradeTestBase class.
*
* @group migrate_drupal_ui
*/
class NoMultilingualReviewPageTest extends NoMultilingualReviewPageTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_translation',
'content_translation',
'datetime_range',
'file',
'language',
'migrate_drupal_ui',
'telephone',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal7.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block',
'Block languages',
'Bulk Export',
'Chaos Tools (CTools) AJAX Example',
'Chaos tools',
'Comment',
'Contact',
'Custom content panes',
'Custom rulesets',
'Dashboard',
'Database logging',
'Date',
'Date All Day',
'Date Context',
'Date Migration',
'Date Popup',
'Date Repeat API',
'Date Repeat Field',
'Date Tools',
'Date Views',
'Email',
'Entity Reference',
'Entity Translation',
'Entity feature module',
'Entity tokens',
'Field translation',
'Field',
'Field SQL storage',
'File',
'Filter',
'Image',
'Internationalization',
'Link',
'List',
'Locale',
'Menu',
'Menu translation',
'Multiupload Filefield Widget',
'Multiupload Imagefield Widget',
'Node',
'Node Reference',
'Number',
'OpenID',
'Options',
'Overlay',
'Page manager',
'Path',
'Phone',
'Poll',
'Profile',
'Search',
'Search embedded form',
'Shortcut',
'String translation',
'Stylizer',
'Synchronize translations',
'System',
'Taxonomy translation',
'Taxonomy',
'Telephone',
'Term Depth access',
'Test search node tags',
'Test search type',
'Text',
'Title',
'User',
'User Reference',
'Variable translation',
'Views UI',
'Views content panes',
// Include modules that do not have an upgrade path and are enabled in the
// source database.
'Blog',
'Content translation',
'Contextual links',
'Date API',
'Entity API',
'Field UI',
'Help',
'PHP filter',
'Testing',
'Toolbar',
'Trigger',
];
}
/**
* {@inheritdoc}
*/
protected function getIncompletePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
'Aggregator',
'Book',
'Breakpoints',
'Color',
'Contact translation',
'Entity Translation Menu',
'Entity Translation Upgrade',
'FlexSlider Picture',
'Forum',
'Multilingual content',
'Multilingual forum',
'Multilingual select',
'Path translation',
'Picture',
'RDF',
'References',
'References UUID',
'Statistics',
'Translation redirect',
'Translation sets',
'User mail translation',
'Variable',
'Variable admin',
'Variable realm',
'Variable store',
'Variable views',
'Views',
'migrate_status_active_test',
// These modules are in the missing path list because they are installed
// on the source site but they are not installed on the destination site.
'Syslog',
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
'Tracker',
'Update manager',
];
}
}

View File

@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\node\Entity\Node;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
use Drupal\user\Entity\User;
// cspell:ignore Filefield Multiupload Imagefield
/**
* Tests Drupal 7 upgrade using the migrate UI.
*
* The test method is provided by the MigrateUpgradeTestBase class.
*
* @group migrate_drupal_ui
* @group #slow
*/
class Upgrade7Test extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_translation',
'content_translation',
'datetime_range',
'language',
'migrate_drupal_ui',
'telephone',
];
/**
* The entity storage for node.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// @todo remove in https://www.drupal.org/project/drupal/issues/3267040
// Delete the existing content made to test the ID Conflict form. Migrations
// are to be done on a site without content. The test of the ID Conflict
// form is being moved to its own issue which will remove the deletion
// of the created nodes.
// See https://www.drupal.org/project/drupal/issues/3087061.
$this->nodeStorage = $this->container->get('entity_type.manager')
->getStorage('node');
$this->nodeStorage->delete($this->nodeStorage->loadMultiple());
$this->loadFixture($this->getModulePath('migrate_drupal') . '/tests/fixtures/drupal7.php');
$this->expectedLoggedErrors = 27;
// If saving the logs, then set the admin user.
if ($this->outputLogs) {
$this->migratedAdminUserName = 'admin';
}
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [
'block' => 27,
'block_content' => 1,
'block_content_type' => 1,
'comment' => 4,
// The 'standard' profile provides the 'comment' comment type, and the
// migration creates 6 comment types, one per node type.
'comment_type' => 9,
// Module 'language' comes with 'en', 'und', 'zxx'. Migration adds 'is'
// and 'fr'.
'configurable_language' => 5,
'contact_form' => 3,
'contact_message' => 0,
'editor' => 2,
'field_config' => 90,
'field_storage_config' => 69,
'file' => 3,
'filter_format' => 7,
'image_style' => 7,
'language_content_settings' => 24,
'node' => 7,
'node_type' => 8,
'search_page' => 3,
'shortcut' => 6,
'shortcut_set' => 2,
'action' => 24,
'menu' => 7,
'taxonomy_term' => 25,
'taxonomy_vocabulary' => 8,
'path_alias' => 8,
'user' => 4,
'user_role' => 4,
'menu_link_content' => 12,
'view' => 14,
'date_format' => 12,
'entity_form_display' => 23,
'entity_form_mode' => 1,
'entity_view_display' => 33,
'entity_view_mode' => 11,
'base_field_override' => 2,
];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
$counts = $this->getEntityCounts();
$counts['block_content'] = 2;
$counts['comment'] = 5;
$counts['file'] = 4;
$counts['menu_link_content'] = 13;
$counts['node'] = 8;
$counts['taxonomy_term'] = 26;
$counts['user'] = 5;
return $counts;
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block languages',
'Block',
'Chaos tools',
'Comment',
'Contact',
'Content translation',
'Database logging',
'Date',
'Email',
'Entity Reference',
'Entity Translation',
'Field SQL storage',
'Field translation',
'Field',
'File',
'Filter',
'Image',
'Internationalization',
'Locale',
'Link',
'List',
'Menu',
'Menu translation',
'Multiupload Filefield Widget',
'Multiupload Imagefield Widget',
'Node',
'Node Reference',
'Number',
'Options',
'Path',
'Phone',
'Search',
'Shortcut',
'String translation',
'Synchronize translations',
'System',
'Taxonomy translation',
'Taxonomy',
'Telephone',
'Text',
'Title',
'User',
'User Reference',
'Variable translation',
// Include modules that do not have an upgrade path and are enabled in the
// source database.
'Blog',
'Contextual links',
'Date API',
'Entity API',
'Field UI',
'Help',
'PHP filter',
'Testing',
'Toolbar',
'Trigger',
];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
'Aggregator',
'Book',
'Color',
'Forum',
'RDF',
'References',
'Statistics',
'Translation sets',
'Variable realm',
'Variable store',
'Variable',
// These modules are in the missing path list because they are installed
// on the source site but they are not installed on the destination site.
'Syslog',
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
'Tracker',
'Update manager',
];
}
/**
* Executes all steps of migrations upgrade.
*/
public function testUpgradeAndIncremental(): void {
// Perform upgrade followed by an incremental upgrade.
$this->doUpgradeAndIncremental();
// Ensure a migrated user can log in.
$this->assertUserLogIn(2, 'a password');
$this->assertFollowUpMigrationResults();
$this->assertEntityRevisionsCount('node', 19);
$this->assertEmailsSent();
$this->assertLogError();
}
/**
* Tests that follow-up migrations have been run successfully.
*
* @internal
*/
protected function assertFollowUpMigrationResults(): void {
$node = Node::load(2);
$this->assertSame('4', $node->get('field_reference')->target_id);
$this->assertSame('6', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('is');
$this->assertSame('4', $translation->get('field_reference')->target_id);
$this->assertSame('4', $translation->get('field_reference_2')->target_id);
$node = Node::load(4);
$this->assertSame('2', $node->get('field_reference')->target_id);
$this->assertSame('2', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('en');
$this->assertSame('2', $translation->get('field_reference')->target_id);
$this->assertSame('2', $translation->get('field_reference_2')->target_id);
$user = User::load(2);
$this->assertSame('2', $user->get('field_reference')->target_id);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Functional\d7;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\WorkflowInterface;
/**
* Tests Drupal 7 upgrade using the migrate UI with Content Moderation.
*
* @group migrate_drupal_ui
* @group #slow
*/
class Upgrade7TestWithContentModeration extends Upgrade7Test {
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_moderation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Set up a moderation flow.
$types = [
'blog',
'et',
'test_content_type',
];
foreach ($types as $type) {
$this->drupalCreateContentType(['type' => $type]);
}
$editorial = Workflow::load('editorial');
assert($editorial instanceof WorkflowInterface);
$type_settings = $editorial->getTypePlugin()->getConfiguration();
$type_settings['default_moderation_state'] = 'published';
$type_settings['entity_types']['node'] = array_merge(
['article'],
$types
);
$type_plugin = $editorial->getTypePlugin();
$type_plugin->setConfiguration($type_settings);
$editorial->trustData()->save();
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
$entity_counts = parent::getEntityCounts() + [
'content_moderation_state' => 5,
'workflow' => 1,
];
$entity_counts['entity_view_display'] = $entity_counts['entity_view_display'] + 1;
$entity_counts['field_config'] = $entity_counts['field_config'] + 2;
$entity_counts['view'] = $entity_counts['view'] + 1;
return $entity_counts;
}
}

View File

@@ -0,0 +1 @@
********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
// cspell:ignore drupalmysqldriverdatabasemysql
/**
* Tests migrate upgrade credential form with settings in settings.php.
*
* @group migrate_drupal_ui
*/
class SettingsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_drupal',
'migrate_drupal_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Log in as user 1. Migrations in the UI can only be performed as user 1.
$this->drupalLogin($this->rootUser);
}
/**
* Test the Credential form with defaults in settings.php.
*
* @param string|null $source_connection
* The value for the source_connection select field.
* @param string $version
* The legacy Drupal version.
* @param string[] $manual
* User entered form values.
* @param string[] $databases
* Databases data or the settings array.
* @param string $expected_source_connection
* The expected source database connection key.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
* @throws \Behat\Mink\Exception\ExpectationException
*
* @dataProvider providerTestCredentialForm
*/
public function testCredentialForm($source_connection, $version, array $manual, array $databases, $expected_source_connection): void {
// Write settings.
$migrate_file_public_path = '/var/www/drupal7/sites/default/files';
$migrate_file_private_path = '/var/www/drupal7/sites/default/files/private';
$settings['settings']['migrate_source_version'] = (object) [
'value' => $version,
'required' => TRUE,
];
$settings['settings']['migrate_source_connection'] = (object) [
'value' => $source_connection,
'required' => TRUE,
];
$settings['settings']['migrate_file_public_path'] = (object) [
'value' => $migrate_file_public_path,
'required' => TRUE,
];
$settings['settings']['migrate_file_private_path'] = (object) [
'value' => $migrate_file_private_path,
'required' => TRUE,
];
foreach ($databases as $key => $value) {
$settings['databases'][$key]['default'] = (object) [
'value' => $value['default'],
'required' => TRUE,
];
}
$this->writeSettings($settings);
$edits = [];
// Enter the values manually if provided.
if (!empty($manual)) {
$edit = [];
$driver = 'Drupal\\mysql\\Driver\\Database\\mysql';
$edit[$driver]['host'] = $manual['host'];
$edit[$driver]['database'] = $manual['database'];
$edit[$driver]['username'] = $manual['username'];
$edit[$driver]['password'] = $manual['password'];
$edits = $this->translatePostValues($edit);
}
// Start the upgrade process.
$this->drupalGet('/upgrade');
$this->submitForm([], 'Continue');
$session = $this->assertSession();
// The source connection field is only displayed when there are connections
// other than default.
if (empty($databases)) {
$session->fieldNotExists('source_connection');
}
else {
$session->fieldExists('source_connection');
}
// Submit the Credential form.
$this->submitForm($edits, 'Review upgrade');
// Confirm that the form actually submitted. IF it submitted, we should see
// error messages about reading files. If there is no error message, that
// indicates that the form did not submit.
$session->responseContains('Failed to read from Document root');
// Assert the form values.
$session->fieldValueEquals('version', $version);
// Check the manually entered credentials or simply the database key.
if (empty($manual)) {
$session->fieldValueEquals('source_connection', $expected_source_connection);
}
else {
$session->fieldValueEquals('edit-drupalmysqldriverdatabasemysql-host', $manual['host']);
$session->fieldValueEquals('edit-drupalmysqldriverdatabasemysql-database', $manual['database']);
$session->fieldValueEquals('edit-drupalmysqldriverdatabasemysql-username', $manual['username']);
}
// Confirm the file paths are correct.
$session->fieldValueEquals('d6_source_base_path', $migrate_file_public_path);
$session->fieldValueEquals('source_base_path', $migrate_file_public_path);
$session->fieldValueEquals('source_private_file_path', $migrate_file_private_path);
}
/**
* Data provider for testCredentialForm.
*/
public static function providerTestCredentialForm() {
return [
'no values in settings.php' => [
'source_connection' => "",
'version' => '7',
'manual' => [
'host' => '172.18.0.2',
'database' => 'drupal7',
'username' => 'kate',
'password' => 'pwd',
],
'databases' => [],
'expected_source_connection' => '',
],
'single database in settings, migrate' => [
'source_connection' => 'migrate',
'version' => '7',
'manual' => [],
'databases' => [
'migrate' => [
'default' => [
'database' => 'drupal7',
'username' => 'user',
'password' => 'pwd',
'prefix' => 'test',
'host' => '172.18.0.3',
'port' => '3307',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
],
],
],
'expected_source_connection' => 'migrate',
],
'migrate_source_connection not set' => [
'source_connection' => '',
'version' => '7',
'manual' => [],
'databases' => [
'migrate' => [
'default' => [
'database' => 'drupal7',
'username' => 'user',
'password' => 'pwd',
'prefix' => 'test',
'host' => '172.18.0.3',
'port' => '3307',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
],
],
],
'expected_source_connection' => 'migrate',
],
'single database in settings, legacy' => [
'source_connection' => 'legacy',
'version' => '6',
'manual' => [],
'databases' => [
'legacy' => [
'default' => [
'database' => 'drupal6',
'username' => 'user',
'password' => 'pwd',
'prefix' => 'test',
'host' => '172.18.0.6',
'port' => '3307',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
],
],
],
'expected_source_connection' => 'legacy',
],
'two databases in settings' => [
'source_connection' => 'source2',
'version' => '7',
'manual' => [],
'databases' => [
'migrate' => [
'default' => [
'database' => 'drupal7',
'username' => 'user',
'password' => 'pwd',
'prefix' => 'test',
'host' => '172.18.0.3',
'port' => '3307',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
],
],
'legacy' => [
'default' => [
'database' => 'site',
'username' => 'user',
'password' => 'pwd',
'prefix' => 'test',
'host' => '172.18.0.2',
'port' => '3307',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
],
],
],
'expected_source_connection' => 'migrate',
],
'database in settings, but use manual' => [
'source_connection' => '',
'version' => '7',
'manual' => [
'host' => '172.18.0.2',
'database' => 'drupal7',
'username' => 'kate',
'password' => 'pwd',
],
'databases' => [
'legacy' => [
'default' => [
'database' => 'site',
'username' => 'user',
'password' => 'pwd',
'prefix' => 'test',
'host' => '172.18.0.2',
'port' => '3307',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
],
],
],
'expected_source_connection' => '',
],
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate_drupal_ui\Kernel;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
/**
* Tests that labels exist for all migrations.
*
* @group migrate_drupal_ui
*/
class MigrationLabelExistTest extends MigrateDrupalTestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
/**
* Tests that labels exist for all migrations.
*/
public function testLabelExist(): void {
// Install all available modules.
$module_handler = $this->container->get('module_handler');
$modules = $this->coreModuleListDataProvider();
$modules_enabled = $module_handler->getModuleList();
$modules_to_enable = array_keys(array_diff_key($modules, $modules_enabled));
$this->enableModules($modules_to_enable);
/** @var \Drupal\migrate\Plugin\MigrationPluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migration');
// Get all the migrations
$migrations = $plugin_manager->createInstances(array_keys($plugin_manager->getDefinitions()));
/** @var \Drupal\migrate\Plugin\Migration $migration */
foreach ($migrations as $migration) {
$migration_id = $migration->getPluginId();
$this->assertNotEmpty($migration->label(), "Label found for $migration_id.");
}
}
}