cforge-2.0.x-dev/src/EventSubscriber/MigrationSubscriber.php
src/EventSubscriber/MigrationSubscriber.php
<?php
namespace Drupal\cforge\EventSubscriber;
use Drupal\mcapi\Entity\Storage\WalletStorage;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Row;
use Drupal\user\Entity\User;
use Drupal\user\Entity\Role;
use Drupal\field\Entity\FieldConfig;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Menu\MenuTreeParameters;
/**
* Migration modifications
*
* Note the order of events in MigrateExecutable::import
* Dispatch PRE_IMPORT
* $source->next
* $source->prepareRow
* hook_migrate_prepare_row
* hook_migrate_NAME_prepare_row
* while
* processRow
* Dispatch PRE_ROW_SAVE
* Dispatch POST_ROW_SAVE
* Dispatch POST_IMPORT
*
*/
class MigrationSubscriber implements EventSubscriberInterface {
private $moduleHandler;
private $moduleInstaller;
private $accountSwitcher;
private $entityTypeManager;
private $configFactory;
private $menuTreeStorage;
/**
* @todo move this to the migration definitions in the relevant modules
*/
const FILTER_FORMATS = [
'editor_full_html' => 'full_html',
'full_html' => 'full_html',
'editor_filtered_html' => 'basic_html',
'filtered_html' => 'basic_html',
'plain_text' => '', // This is the fallback
'php_code' => '', // This will be ignored
'' => 'full_html'
];
/**
* @todo move this to the migration definitions in the relevant modules
*/
const VOCABS = [
'cforge_docs_categories' => 'binders',
'offers_wants_types' => NULL,
'offers_wants_categories' => 'category',
'galleries' => 'galleries'
];
function __construct($module_handler, $module_installer, $account_switcher, $config_factory, $entity_type_manager, $menu_tree_storage) {
$this->moduleHandler = $module_handler;
$this->moduleInstaller = $module_installer;
$this->accountSwitcher = $account_switcher;
$this->configFactory = $config_factory;
$this->entityTypeManager = $entity_type_manager;
$this->menuTreeStorage = $menu_tree_storage;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() : array {
return [
'migrate.pre_import' => [['migratePreImport']],
'migrate.pre_row_save' => [['migratePreRowSave']],
'migrate.post_row_save' => [['migratePostRowSave']],
'migrate.post_import' => [['migratePostImport']]
];
}
/**
* @param Drupal\migrate\Event\MigrateImportEvent $event
*/
public function migratePreImport(MigrateImportEvent $event) {
$migration = $event->getMigration();
switch ($migration->id()) {
case 'd7_field_instance':
// Find any duplicate field instances
$db = $migration->getSourcePlugin()->getDatabase();
$dupes = $db->query("SELECT min([id]) FROM {field_config_instance} WHERE [deleted] = 0 GROUP BY [entity_type], [field_name], [bundle] HAVING count([id]) > 1")->fetchCol();
if ($dupes) {
// Delete them from the ORIGINAL database
$db->delete('field_config_instance')->condition('id', $dupes, 'IN')->execute();
}
break;
case 'd7_user':
// Because the wallet names based on user names are wrong if user is not logged in.
$this->accountSwitcher->switchTo(User::load(1));
break;
case 'd7_node_complete:forum':
if ($this->moduleHandler->moduleExists('forum_access')) {
$this->moduleInstaller->install(['forum_access_migrate']);
}
break;
}
}
/**
* @param Drupal\migrate\Event\MigratePreRowSaveEvent $event
*/
public function migratePreRowSave(MigratePreRowSaveEvent $event) {
$row = $event->getRow();
$migration = $event->getMigration();
// Ensure the category->required setting is transferred.
if ($migration->id() == 'd7_field_instance' and $row->getSourceProperty('field_name') == 'offers_wants_categories' and $row->getSourceProperty('entity_type') == 'transaction') {
// Just take this one property which varied in some d7 sites then ditch the field
$field = FieldConfig::load('mcapi_transaction.mcapi_transaction.category');
$field->setRequired($row->getSourceProperty('required'))->save();
}
if ($migration->id() == 'd7_user') {
$address = $row->getSourceProperty('profile_address');
$dest_address = $this->transformAddress($address[0]);
$row->setDestinationProperty('address', [$dest_address]);
$row->removeDestinationProperty('profile_address');
$phones = $row->getSourceProperty('profile_phones');
$contactme = ['mob' => $phones[0]['value']];
if (isset($phones[1])) {
$contactme['tel'] = $phones[1]['value'];
}
$row->setDestinationProperty('contactme', $contactme);
}
if ($migration->id() == 'd7_field' or $migration->id() == 'd7_field_instance') {
//Skip these d7 fields and rename them when importing the entities they belong to.
$existing_fields = [
'manage_notes',
'manage_responsibility',
'profile_address',
'profile_notes',
'profile_phones',
'profile_location',
'upload', //became 'attached'
'upload_private', // Became 'attached_private',
'user_picture'
//'title', // Prevents 'base_field_override' fields being created
];
if (in_array($row->getSourceProperty('field_name'), $existing_fields)) {
throw new MigrateSkipRowException('Field has already installed equivalent');
}
if ($row->getSourceProperty('field_name') == 'body' and $row->getSourceProperty('entity_type') == 'node') {
throw new MigrateSkipRowException('Node body fields not migrated');
}
if ($row->getSourceProperty('field_name') == 'manage_responsibility') {
throw new MigrateSkipRowException("Field manage_responsibility is discontinued");
}
if ($row->getSourceProperty('field_name') == 'offers_wants_categories') {
throw new MigrateSkipRowException('offers_wants_categories replacement is already installed by the smallads module');
}
}
// All novel user fields should go on the 'profile' form display and 'default' display
if ($migration->getPluginId() == 'd7_field_instance' and $row->getSourceProperty('entity_type') == 'user') {
$formDisplay = EntityFormDisplay::load('user.user.profile');
$formDisplay->setComponent($row->getSourceProperty('field_name'), ['weight' => 20])->save();
$viewDisplay = EntityViewDisplay::load('user.user.default');
$viewDisplay->setComponent($row->getSourceProperty('field_name'), ['weight' => 20])->save();
}
if ($migration->id() == 'd7_user') {
// Don't migrate blocked users.
if (!$row->getSourceProperty('status')) {
if (!$this->hasTraded($migration->getSourcePlugin()->getDatabase(), $row->getSourceProperty('status'))) {
throw new MigrateSkipRowException('Skipping blocked user '.$row->getSourceProperty('uid'));
}
}
// Rename the fields
$map = [
'manage_notes' => 'notes_admin',
'profile_notes' => 'notes',
'profile_location' => 'coordinates', // will only save if cforge_geo
];
foreach ($map as $old_name => $new_name) {
if ($row->hasSourceProperty($old_name)) {
$this->mapField($row, $old_name, $new_name);
}
}
// Experimental
$row->setDestinationProperty('timezone', NULL);
// the commitee and local admin roles were not migrated so are not available for user migration to look up.
if (in_array(4, $row->getSourceProperty('roles'))) {
$dest_roles = $row->getDestinationProperty('roles');
$dest_roles[] = 'committee';
$row->setDestinationProperty('roles', $dest_roles);
}
$address = $row->getSourceProperty('profile_address');
$dest_address = $this->transformAddress($address[0]);
$row->setDestinationProperty('address', [$dest_address]);
$row->removeDestinationProperty('profile_address');
}
if ($migration->id() == 'd7_mcapi_transaction') {
$this->mapField($row, 'offers_wants_categories', 'category');
}
if ($migration->id() == 'd7_menu') {
if ($row->getSourceProperty('menu_name') == 'visitors') {
throw new MigrateSkipRowException('Not migrating visitors menu.');
}
}
// Move all terms that used to be in offers_wants_categories field.
if ($migration->getPluginId() == 'd7_node' or $migration->id() == 'd7_mcapi_transaction') {
if ($row->hasDestinationProperty('offers_wants_categories')) {
$entity_type = $row->getSourceProperty('entity_type');
$new_field_name = $entity_type == 'node' ? 'categories' : 'category';
$this->mapField($row, 'offers_wants_categories', $new_field_name);
}
}
// Change the filter format for every body field
// I don't know why this doesn't happen already
if ($row->getSourceProperty('plugin') == 'd7_node') {
$this->entityBodyFilterFormat($row, 'body');
if ($row->hasSourceProperty('upload')) {
$this->mapFileField($row, 'upload', 'attached');
}
if ($row->hasSourceProperty('upload_private')) {
$this->mapFileField($row, 'upload_private', 'attached_private');
}
}
elseif ($row->getSourceProperty('plugin') == 'd7_smallad') {
$this->entityBodyFilterFormat($row, 'body');
}
elseif ($migration->id() == 'd7_custom_block') {
// I don't know why this isn't happening automatically - perhaps a bug in d8.4?
$row->setDestinationProperty('body/format', $this->convertFormat($row->getDestinationProperty('body/format'), 'full_html'));
}
// Comments
elseif ($migration->id() == 'd7_comment_type') {
if ($row->getSourceProperty('type') != 'forum') {
throw new MigrateSkipRowException('Comments already exist');
}
}
elseif ($migration->id() == 'd7_comment_field') {
$row->setDestinationProperty('type', 'comments');
}
elseif ($migration->id() == 'd7_comment') {
$this->entityBodyFilterFormat($row, 'comment_body');
if ($row->getDestinationProperty('entity_type') == 'node') {
if ($row->getSourceProperty('node_type') != 'forum') {
$row->setDestinationProperty('comment_type', $row->getDestinationProperty('entity_type'));
$row->setDestinationProperty('field_name', 'comments');
}
}
}
// Fix an error I can't explain
elseif ($migration->id() == 'd7_field_formatter_settings') {
if (empty($row->getDestinationProperty('view_mode'))) {
throw new MigrateSkipRowException('Skipping because view mode is lost.');
}
}
// Since we're not migrating the address field, we lose the country value in
// its widget settings, so here we try to take the country from the d7 site
// variable.
//@todo ensure that d7 sites have the site_default_country variable populated..
elseif ($migration->id() == 'd7_system_date') {
// France as a default is a guess based on usage stags
$default_country = $row->getSourceProperty('site_default_country');
if (strlen($default_country) <> 2){
\Drupal::logger('cforge')->error(print_r($row->getSource(), 1));
die(' -- Invalid country code! died.');
}
// @todo some sites don't have this var set so what to do?
FieldConfig::load('user.user.address')
->setSetting('available_countries', [$default_country])
->setSetting('default_country', $default_country)
->save();
}
elseif ($migration->id() == 'd7_user_role') {
$id = $row->getDestinationProperty('id');
if ($id == 'anonymous' or $id == 'authenticated') {
$permissions = $row->getDestinationProperty('permissions');
$permissions[] = 'use text format basic_html';
$row->setDestinationProperty('permissions', array_unique($permissions));
}
}
// This fixes a wierd incompatibility that just happens with a few blocks, leaving them with hidden titles.
elseif ($migration->id() == 'd7_block') {
if ($row->getSourceProperty('module') == 'user') {
if ($row->getSourceProperty('delta') == 'new') {
$settings = $row->getDestinationProperty('settings');
if ($row->getSourceProperty('title') == '' and !$settings['label_display']) {
$settings['label_display'] = 1;
$row->setDestinationProperty('settings', $settings);
}
}
}
else {
throw new MigrateSkipRowException();
}
}
// Only migrate from certain menus
elseif ($migration->id() == 'd7_menu_links') {
// Move everything in visitors menu to main menu.
if ($row->getSourceProperty('menu_name') == 'visitors') {
$row->setDestinationProperty('menu_name', 'main');
}
// don't migrate unknown menus.
$menu_name = $row->getSourceProperty('menu_name');
if (!in_array($menu_name, ['footer', 'main-menu', 'secondary-menu', 'visitors'])) {
throw new MigrateSkipRowException('Not migrating '.$row->getSourceProperty('link_path').' in '.$menu_name);
}
else {
// Don't migrate if a link already exists in the same menu with the same title.
$title = $row->getSourceProperty('link_title');
$tree = $this->menuTreeStorage
->loadTreeData($row->getDestinationProperty('menu_name'), new MenuTreeParameters());
foreach ($tree['tree'] as $id => $item) {
if ($item['definition']['title'] == $title) {
throw new MigrateSkipRowException($row->getSourceProperty('link_path') . ' already exists as a menu item');
}
}
}
}
elseif ($migration->id() == 'd7_system_theme??') {
// I can't see which migration contains the admin theme, which should be set to claro.
}
}
/**
* @param MigratePostRowSaveEvent $event
* Save the user's avatar image which didn't migrate automatically
*/
function migratePostRowSave(MigratePostRowSaveEvent $event) {
$mig_id = $event->getMigration()->id();
$row = $event->getRow();
if ($mig_id == 'd7_user') {
if ($pic_id = $row->getSourceProperty('picture')) {
User::load($row->getSourceProperty('uid'))
->set('user_picture', $pic_id)
->save();
}
}
elseif ($mig_id == 'd7_menu_links') {
$old_name = $row->getSourceProperty('menu_name');
// set the public flag on nodes with menu items from secondary-menu or visitors.
if ($old_name == 'secondary-menu'or $old_name == 'visitors') {
$path = $row->getSourceProperty('link_path');
if (preg_match('/node\/([0-9]+)/', $path, $matches)) {
\Drupal::keyValue('publiconly')->set($matches[1], 1);
}
}
}
elseif ($mig_id == 'd7_cforge_settings') {
// Todo test this...
if ($fee = $row->getSourceProperty('cforge_referrer_fee')) {
print "\n check migration of referrer fee in \Drupal\cforge\MigrationSubscriber::migratePostRowSave\n";
print print_r($row->getSource(), 1);
if ($fee['value']) {
\Drupal::service('module_installer')->install(['cforge_referrer']);
}
}
}
}
/**
* @param MigrateImportEvent $event
*/
function migratePostImport(MigrateImportEvent $event) {
switch ($event->getMigration()->id()) {
case 'd7_filter_format':
$this->configFactory->getEditable('filter.settings')->set('fallback_format', 'plain_text');
break;
case 'd7_mcapi_transaction':
$this->removeSystemRole();
break;
case 'd7_node_complete:forum':
$this->moduleInstaller->uninstall(['forum_access_migrate']);
break;
case 'd7_node:page':
\Drupal::moduleHandler()->loadInclude('cforge', 'install');
cforge_import_content('cforge', 'all'); // 403 and 403 Pages
}
}
/**
* Delete all the accounts with the system role and delete the system role.
* @note this assumes that wallets have been created.
*/
private function removeSystemRole() {
$user_storage = $this->entityTypeManager->getStorage('user');
$system_users = $user_storage->loadByProperties(['roles' => 'system']);
$committee = $user_storage->loadByProperties(['roles' => 'committee']);
$ladmins = $user_storage->loadByProperties(['roles' => 'local admin']);
unset($system_users[1], $committee[1], $ladmins[1]);
$ladmin = reset($ladmins) or $ladmin = array_shift($committee);
foreach ($system_users as $account) {
foreach (WalletStorage::walletsOf($account, TRUE) as $wallet) {
// Move it to local admin, with committee members having access
$wallet->set('holder', $ladmin);
if ($this->moduleHandler->moduleExists('mcapi_bursers')) {
foreach ($committee as $comm) {
$wallet->bursers->addBurser($comm);
}
$wallet->save();
}
print "\nChanged owner and added bursers to former system wallet ".$wallet->label();
}
print "\nDeleted former system user ".$account->getDisplayName();
$account->delete();
}
if ($role = Role::load('system')){
$role->delete();
print "\nDeleted system role";
}
}
/**
* Change the destination name of a field.
* @param Row $row
* @param string $old_name
* @param string $new_name
*/
private function mapField(Row $row, string $old_name, string $new_name) {
$row->setDestinationProperty($new_name, $row->getSourceProperty($old_name));
$row->removeDestinationProperty($old_name);
}
private function mapFileField(Row $row, string $old_name, string $new_name) {
$this->mapField($row, $old_name, $new_name);
$files = $row->getDestinationProperty($new_name);
foreach ($files as &$file) {
$file['target_id'] = $file['fid'];
}
$row->setDestinationProperty($new_name, $files);
}
/**
* Convert the filter format of a body field
* @param Row $row
* @param string $body_field_name
* @param type $default
*/
private function entityBodyFilterFormat(Row $row, $body_field_name, $default = 'basic_html') {
if ($bods = $row->getDestinationProperty($body_field_name)) {
foreach ($bods as $delta => &$body) {
$old = $body['format']?? 'filtered_html';
if (strpos($body['value'], '<embed')) {
$old = 'full_html';
}
$body['format'] = $this->convertFormat($old, $default);
}
$row->setDestinationProperty($body_field_name, $bods);
}
}
/**
* Look up the new filter format witih the old
*/
private function convertFormat($old_format_name, $default) {
return static::FILTER_FORMATS[$old_format_name] ?? $default;
}
private function hasTraded($db, $uid) {
return $db->query("SELECT count([xid]) FROM mcapi_transactions WHERE [payer] = $uid or [payee] = $uid")->fetchField();
}
// All this is necessary because the address/src/Plugin/migrate/process/AddressField.php isn't running
private function transformAddress($value) {
$parsed = [
'country_code' => $value['country'],
'administrative_area' => $value['administrative_area'],
'locality' => $value['locality'],
'dependent_locality' => $value['dependent_locality'],
'postal_code' => $value['postal_code'],
'sorting_code' => '',
'address_line1' => $value['thoroughfare'],
'address_line2' => $value['premise'],
'organization' => $value['organisation_name'],
];
if (!empty($value['first_name']) || !empty($value['last_name'])) {
$parsed['given_name'] = $value['first_name'];
$parsed['family_name'] = $value['last_name'];
}
elseif (!empty($value['name_line'])) {
$split = explode(" ", $value['name_line']);
$parsed['given_name'] = array_shift($split);
$parsed['family_name'] = implode(' ', $split);
}
return $parsed;
}
}
