diff --git a/README.txt b/README.txt index da936bf..673e4e7 100644 --- a/README.txt +++ b/README.txt @@ -61,7 +61,7 @@ The primary features include: APIs, but they are not needed by most sites and have no bearing on the Open Graph meta tags. -* The Pinterest meta tags may be added by enabling the "Metatag: Pinterest" +* The Pinterest meta tags may be added by enabling the "Metatag: Pinterest" submodule. * Site verification meta tags can be added, e.g. as used by the Google search @@ -230,6 +230,24 @@ the type "html_tag". Extracting the value of the meta tag will depend upon the type of meta tag, e.g. the generator meta tag uses the "content" attribute while the link tag uses the "href" attribute. +Migration +-------------------------------------------------------------------------------- +An upgrade path from Drupal 7 is provided. The supported migration workflows are +Drupal core's "Migrate Drupal UI" wizard, and the Drush commands provided by +Migrate Tools (https://www.drupal.org/project/migrate_tools); other scenarios, +including custom migrations, may not work as expected. + +To handle a custom migration from Drupal 7 to Drupal 8, add the field mapping +into the appropriate migrate_plus.migration yml file. e.g.: + +process: +... + field_metatags: pseudo_field_metatag +.. + +Note: Replace `field_metatags` with the machine name used on the destination +entity type. + DrupalConsole integration -------------------------------------------------------------------------------- diff --git a/metatag.api.php b/metatag.api.php index cd2ad01..86041f4 100644 --- a/metatag.api.php +++ b/metatag.api.php @@ -57,3 +57,14 @@ function hook_metatags_attachments_alter(array &$metatag_attachments) { } } } + +/** + * Allow the list of Metatag D7's tags to be changed. + * + * @param array $tags_map + * An array of D7 tag names mapped against the D8 tag's IDs. + */ +function hook_metatag_migrate_tags_map_alter($tags_map) { + // This tag was renamed in D8. + $tags_map['custom:tag'] = 'custom_tag'; +} diff --git a/metatag.module b/metatag.module index 243a1f5..e4d9c7b 100644 --- a/metatag.module +++ b/metatag.module @@ -5,6 +5,8 @@ * Contains metatag.module. */ +use Drupal\Component\Plugin\Factory\DefaultFactory; +use Drupal\Component\Utility\Html; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -13,8 +15,15 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; +use Drupal\migrate\Plugin\migrate\destination\EntityContentBase; +use Drupal\migrate\Plugin\MigrateSourceInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Drupal\node\Plugin\migrate\source\d7\Node; +use Drupal\taxonomy\Plugin\migrate\source\d7\Term; use Drupal\taxonomy\TermInterface; -use Drupal\Component\Utility\Html; +use Drupal\user\Plugin\migrate\source\d7\User; +use Drupal\metatag\MigrateHelper; /** * Implements hook_help(). @@ -614,3 +623,125 @@ function metatag_generate_entity_metatags($entity) { } return $values; } + +/** + * Implements hook_migrate_prepare_row(). + */ +function metatag_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) { + // If there's no 'metatag' table in the source, there's no point in + // continuing. + if (!$source->getDatabase()->schema()->tableExists('metatag')) { + return; + } + + // @todo Write a more general version rather than hard-coded. + // Support a know subset of D7 sources. + if (is_a($source, Node::class)) { + // E.g. d7_node, d7_node_revision. + $source_type = 'node'; + } + elseif (is_a($source, Term::class)) { + // E.g. d7_taxonomy_term. + $source_type = 'taxonomy'; + } + elseif (is_a($source, User::class)) { + // E.g. d7_user. + $source_type = 'user'; + } + else { + // Not supported now, nothing to do. + return; + } + + if ($migration->getDestinationPlugin() instanceof EntityContentBase) { + $entity_type = NULL; + $entity_id = NULL; + $revision_id = NULL; + + // @todo Write a more general version rather than a switch statement. + switch ($source_type) { + case 'node': + $entity_type = 'node'; + $entity_id = $row->getSourceProperty('nid'); + $revision_id = $row->getSourceProperty('vid'); + break; + + case 'taxonomy': + $entity_type = 'taxonomy_term'; + $entity_id = $row->getSourceProperty('tid'); + break; + + case 'user': + $entity_type = 'user'; + $entity_id = $row->getSourceProperty('uid'); + break; + } + + /** @var \Drupal\migrate\Plugin\migrate\source\SqlBase $source */ + /** @var \Drupal\Core\Database\Query\SelectInterface $query */ + $query = $source->getDatabase()->select('metatag', 'm') + ->fields('m', ['data']) + ->condition('entity_type', $entity_type) + ->condition('entity_id', $entity_id); + if (!is_null($revision_id)) { + if ($source->getDatabase()->schema()->fieldExists('metatag', 'revision_id')) { + $query->condition('revision_id', $revision_id); + } + } + + // Keep a list of the D7 meta tags and how they translate to D8. + $tags_map = MigrateHelper::mapForMetatagD7(); + + $metatags = []; + foreach ($query->execute()->fetchCol() as $data_entry) { + // Re-shape D7 entries into for D8 entries. + // @todo This could live in a migrate process plugin. + foreach (unserialize($data_entry) as $d7_metatag_name => $data) { + // If there's no data for this tag, ignore everything. + if (empty($data)) { + continue; + } + + // @todo Skip these values for now, maybe an some version supported + // these? + if (!is_array($data) || empty($data['value'])) { + continue; + } + + // Convert the D7 meta tag name to the D8 equivalent. If this meta tag + // is not recognized, skip it. + if (empty($tags_map[$d7_metatag_name])) { + continue; + } + $d8_metatag_name = $tags_map[$d7_metatag_name]; + + // Keep the entire data structure. + $metatags[$d8_metatag_name] = $data; + } + } + + $row->setSourceProperty('pseudo_field_metatag', serialize($metatags)); + } +} + +/** + * Implements hook_migration_plugins_alter(). + */ +function metatag_migration_plugins_alter(array &$migrations) { + foreach ($migrations as &$migration) { + if (isset($migration['destination']['plugin'])) { + // Follow logic on hook_entity_base_field_info(), and exclude metatag + // itself. + if (in_array($migration['destination']['plugin'], ['entity:comment', 'entity:metatag'])) { + continue; + } + $plugin_definition = \Drupal::service('plugin.manager.migrate.destination') + ->getDefinition($migration['destination']['plugin']); + $destination_plugin = DefaultFactory::getPluginClass($migration['destination']['plugin'], $plugin_definition); + + if (is_subclass_of($destination_plugin, EntityContentBase::class) || $destination_plugin == EntityContentBase::class) { + $migration['process']['field_metatag'] = 'pseudo_field_metatag'; + } + } + } +} diff --git a/migrations/d7_metatag_field.yml b/migrations/d7_metatag_field.yml new file mode 100644 index 0000000..0b6f0cd --- /dev/null +++ b/migrations/d7_metatag_field.yml @@ -0,0 +1,20 @@ +id: d7_metatag_field +label: Metatag field +migration_tags: + - Drupal 7 +source: + plugin: d7_metatag_field + ignore_map: true + constants: + status: true + langcode: und + field_name: field_metatag + type: metatag +process: + entity_type: entity_type + status: 'constants/status' + langcode: 'constants/langcode' + field_name: 'constants/field_name' + type: 'constants/type' +destination: + plugin: entity:field_storage_config diff --git a/migrations/d7_metatag_field_instance.yml b/migrations/d7_metatag_field_instance.yml new file mode 100644 index 0000000..f3bf586 --- /dev/null +++ b/migrations/d7_metatag_field_instance.yml @@ -0,0 +1,23 @@ +id: d7_metatag_field_instance +label: Metatag field instance +migration_tags: + - Drupal 7 +source: + plugin: d7_metatag_field_instance + source_module: metatag + ignore_map: true + constants: + field_name: field_metatag + label: Metatags +process: + entity_type: entity_type + field_name: 'constants/field_name' + bundle: bundle + label: 'constants/label' +destination: + plugin: entity:field_config +migration_dependencies: + required: + - d7_metatag_field + - d7_node_type + - d7_taxonomy_vocabulary diff --git a/migrations/d7_metatag_field_instance_widget_settings.yml b/migrations/d7_metatag_field_instance_widget_settings.yml new file mode 100644 index 0000000..2d9a5a2 --- /dev/null +++ b/migrations/d7_metatag_field_instance_widget_settings.yml @@ -0,0 +1,21 @@ +id: d7_metatag_field_instance_widget_settings +label: Metatag field instance widget settings +migration_tags: + - Drupal 7 +source: + plugin: d7_metatag_field_instance + source_module: metatag + ignore_map: true + constants: + form_mode: default + field_name: field_metatag +process: + bundle: bundle + form_mode: 'constants/form_mode' + field_name: 'constants/field_name' + entity_type: entity_type +destination: + plugin: component_entity_form_display +migration_dependencies: + required: + - d7_metatag_field_instance diff --git a/src/MigrateHelper.php b/src/MigrateHelper.php new file mode 100644 index 0000000..929130b --- /dev/null +++ b/src/MigrateHelper.php @@ -0,0 +1,356 @@ + 'abstract', + // @todo 'cache-control' => '', + 'canonical' => 'canonical_url', + 'content-language' => 'content_language', + 'description' => 'description', + // @todo 'expires' => '', + 'generator' => 'generator', + 'geo.placename' => 'geo_placename', + 'geo.position' => 'geo_position', + 'geo.region' => 'geo_region', + 'icbm' => 'icbm', + 'image_src' => 'image_src', + 'keywords' => 'keywords', + 'news_keywords' => 'news_keywords', + // @todo 'next' => '', + 'original-source' => 'original_source', + // @todo 'pragma' => '', + // @todo 'prev' => '', + 'rating' => 'rating', + 'referrer' => 'referrer', + // @todo 'refresh' => '', + // @todo 'revisit-after' => '', + 'rights' => 'rights', + 'robots' => 'robots', + 'set_cookie' => 'set_cookie', + 'shortlink' => 'shortlink', + 'standout' => 'standout', + 'title' => 'title', + // From metatag_app_links.metatag.inc: + 'al:android:app_name' => 'al_android_app_name', + 'al:android:class' => 'al_android_class', + 'al:android:package' => 'al_android_package', + 'al:android:url' => 'al_android_url', + 'al:ios:app_name' => 'al_ios_app_name', + 'al:ios:app_store_id' => 'al_ios_app_store_id', + 'al:ios:url' => 'al_ios_url', + 'al:ipad:app_name' => 'al_ipad_app_name', + 'al:ipad:app_store_id' => 'al_ipad_app_store_id', + 'al:ipad:url' => 'al_ipad_url', + 'al:iphone:app_name' => 'al_iphone_app_name', + 'al:iphone:app_store_id' => 'al_iphone_app_store_id', + 'al:iphone:url' => 'al_iphone_url', + 'al:web:should_fallback' => 'al_web_should_fallback', + 'al:web:url' => 'al_web_url', + 'al:windows:app_id' => 'al_windows_app_id', + 'al:windows:app_name' => 'al_windows_app_name', + 'al:windows:url' => 'al_windows_url', + 'al:windows_phone:app_id' => 'al_windows_phone_app_id', + 'al:windows_phone:app_name' => 'al_windows_phone_app_name', + 'al:windows_phone:url' => 'al_windows_phone_url', + 'al:windows_universal:app_id' => 'al_windows_universal_app_id', + 'al:windows_universal:app_name' => 'al_windows_universal_app_name', + 'al:windows_universal:url' => 'al_windows_universal_url', + // From metatag_dc.metatag.inc: + 'dcterms.contributor' => 'dcterms_contributor', + 'dcterms.coverage' => 'dcterms_coverage', + 'dcterms.creator' => 'dcterms_creator', + 'dcterms.date' => 'dcterms_date', + 'dcterms.description' => 'dcterms_description', + 'dcterms.format' => 'dcterms_format', + 'dcterms.identifier' => 'dcterms_identifier', + 'dcterms.language' => 'dcterms_language', + 'dcterms.publisher' => 'dcterms_publisher', + 'dcterms.relation' => 'dcterms_relation', + 'dcterms.rights' => 'dcterms_rights', + 'dcterms.source' => 'dcterms_source', + 'dcterms.subject' => 'dcterms_subject', + 'dcterms.title' => 'dcterms_title', + 'dcterms.type' => 'dcterms_type', + // From metatag_dc_advanced.metatag.inc: + 'dcterms.abstract' => 'dcterms_abstract', + 'dcterms.accessRights' => 'dcterms_access_rights', + 'dcterms.accrualMethod' => 'dcterms_accrual_method', + 'dcterms.accrualPeriodicity' => 'dcterms_accrual_periodicity', + 'dcterms.accrualPolicy' => 'dcterms_accrual_policy', + 'dcterms.alternative' => 'dcterms_alternative', + 'dcterms.audience' => 'dcterms_audience', + 'dcterms.available' => 'dcterms_available', + 'dcterms.bibliographicCitation' => 'dcterms_bibliographic_citation', + 'dcterms.conformsTo' => 'dcterms_conforms_to', + 'dcterms.created' => 'dcterms_created', + 'dcterms.dateAccepted' => 'dcterms_date_accepted', + 'dcterms.dateCopyrighted' => 'dcterms_date_copyrighted', + 'dcterms.dateSubmitted' => 'dcterms_date_submitted', + 'dcterms.educationLevel' => 'dcterms_education_level', + 'dcterms.extent' => 'dcterms_extent', + 'dcterms.hasFormat' => 'dcterms_has_format', + 'dcterms.hasPart' => 'dcterms_has_part', + 'dcterms.hasVersion' => 'dcterms_has_version', + 'dcterms.instructionalMethod' => 'dcterms_instructional_method', + 'dcterms.isFormatOf' => 'dcterms_is_format_of', + 'dcterms.isPartOf' => 'dcterms_is_part_of', + 'dcterms.isReferencedBy' => 'dcterms_is_referenced_by', + 'dcterms.isReplacedBy' => 'dcterms_is_replaced_by', + 'dcterms.isRequiredBy' => 'dcterms_is_required_by', + 'dcterms.issued' => 'dcterms_issued', + 'dcterms.isVersionOf' => 'dcterms_is_version_of', + 'dcterms.license' => 'dcterms_license', + 'dcterms.mediator' => 'dcterms_mediator', + 'dcterms.medium' => 'dcterms_medium', + 'dcterms.modified' => 'dcterms_modified', + 'dcterms.provenance' => 'dcterms_provenance', + 'dcterms.references' => 'dcterms_references', + 'dcterms.replaces' => 'dcterms_replaces', + 'dcterms.requires' => 'dcterms_requires', + 'dcterms.rightsHolder' => 'dcterms_rights_holder', + 'dcterms.spatial' => 'dcterms_spatial', + 'dcterms.tableOfContents' => 'dcterms_table_of_contents', + 'dcterms.temporal' => 'dcterms_temporal', + 'dcterms.valid' => 'dcterms_valid', + // From metatag_facebook.metatag.inc: + 'fb:admins' => 'fb_admins', + 'fb:app_id' => 'fb_app_id', + 'fb:pages' => 'fb_pages', + // From metatag_favicons.metatag.inc: + 'apple-touch-icon' => 'apple_touch_icon', + 'apple-touch-icon-precomposed' => 'apple_touch_icon_precomposed', + 'apple-touch-icon-precomposed_114x114' => 'apple_touch_icon_precomposed_114x114', + 'apple-touch-icon-precomposed_120x120' => 'apple_touch_icon_precomposed_120x120', + 'apple-touch-icon-precomposed_144x144' => 'apple_touch_icon_precomposed_144x144', + 'apple-touch-icon-precomposed_152x152' => 'apple_touch_icon_precomposed_152x152', + 'apple-touch-icon-precomposed_180x180' => 'apple_touch_icon_precomposed_180x180', + 'apple-touch-icon-precomposed_72x72' => 'apple_touch_icon_precomposed_72x72', + 'apple-touch-icon-precomposed_76x76' => 'apple_touch_icon_precomposed_76x76', + 'apple-touch-icon_114x114' => 'apple_touch_icon_114x114', + 'apple-touch-icon_120x120' => 'apple_touch_icon_120x120', + 'apple-touch-icon_144x144' => 'apple_touch_icon_144x144', + 'apple-touch-icon_152x152' => 'apple_touch_icon_152x152', + 'apple-touch-icon_180x180' => 'apple_touch_icon_180x180', + 'apple-touch-icon_72x72' => 'apple_touch_icon_72x72', + 'apple-touch-icon_76x76' => 'apple_touch_icon_76x76', + 'icon_16x16' => 'icon_16x16', + 'icon_192x192' => 'icon_192x192', + 'icon_32x32' => 'icon_32x32', + 'icon_96x96' => 'icon_96x96', + 'mask-icon' => 'mask-icon', + 'shortcut icon' => 'shortcut_icon', + // From metatag_google_cse.metatag.inc: + 'audience' => 'audience', + 'department' => 'department', + 'doc_status' => 'doc_status', + 'google_rating' => 'google_rating', + 'thumbnail' => 'thumbnail', + // From metatag_google_plus.metatag.inc; not doing these, Google+ closed. + 'itemtype' => '', + 'itemprop:name' => '', + 'itemprop:description' => '', + 'itemprop:image' => '', + 'author' => '', + 'publisher' => '', + // From metatag_hreflang.metatag.inc: + 'hreflang_xdefault' => 'hreflang_xdefault', + // @todo 'hreflang_' . $langcode => 'hreflang_per_language', + // From metatag_mobile.metatag.inc: + // @todo 'alternate_handheld' => '', + // @todo 'amphtml' => '', + 'android-app-link-alternative' => 'android_app_link_alternative', + 'android-manifest' => 'android_manifest', + 'apple-itunes-app' => 'apple_itunes_app', + 'apple-mobile-web-app-capable' => 'apple_mobile_web_app_capable', + 'apple-mobile-web-app-status-bar-style' => 'apple_mobile_web_app_status_bar_style', + 'apple-mobile-web-app-title' => 'apple_mobile_web_app_title', + 'application-name' => 'application_name', + 'cleartype' => 'cleartype', + 'format-detection' => 'format_detection', + 'HandheldFriendly' => 'handheldfriendly', + 'ios-app-link-alternative' => 'ios_app_link_alternative', + 'MobileOptimized' => 'mobileoptimized', + 'msapplication-allowDomainApiCalls' => 'msapplication_allowDomainApiCalls', + 'msapplication-allowDomainMetaTags' => 'msapplication_allowDomainMetaTags', + 'msapplication-badge' => 'msapplication_badge', + 'msapplication-config' => 'msapplication_config', + 'msapplication-navbutton-color' => 'msapplication_navbutton_color', + 'msapplication-notification' => 'msapplication_notification', + 'msapplication-square150x150logo' => 'msapplication_square150x150logo', + 'msapplication-square310x310logo' => 'msapplication_square310x310logo', + 'msapplication-square70x70logo' => 'msapplication_square70x70logo', + 'msapplication-starturl' => 'msapplication_starturl', + 'msapplication-task' => 'msapplication_task', + 'msapplication-task-separator' => 'msapplication_task_separator', + 'msapplication-tilecolor' => 'msapplication_tilecolor', + 'msapplication-tileimage' => 'msapplication_tileimage', + 'msapplication-tooltip' => 'msapplication_tooltip', + 'msapplication-wide310x150logo' => 'msapplication_wide310x150logo', + 'msapplication-window' => 'msapplication_window', + 'theme-color' => 'theme_color', + 'viewport' => 'viewport', + 'x-ua-compatible' => 'x_ua_compatible', + // From metatag_opengraph.metatag.inc: + 'article:author' => 'article_author', + 'article:expiration_time' => 'article_expiration_time', + 'article:modified_time' => 'article_modified_time', + 'article:published_time' => 'article_published_time', + 'article:publisher' => 'article_publisher', + 'article:section' => 'article_section', + 'article:tag' => 'article_tag', + 'book:author' => 'book_author', + 'book:isbn' => 'book_isbn', + 'book:release_date' => 'book_release_date', + 'book:tag' => 'book_tag', + // @todo 'og:audio' => '', + // @todo 'og:audio:secure_url' => '', + // @todo 'og:audio:type' => '', + 'og:country_name' => 'og_country_name', + 'og:description' => 'og_description', + 'og:determiner' => 'og_determiner', + 'og:email' => 'og_email', + 'og:fax_number' => 'og_fax_number', + 'og:image' => 'og_image', + // @todo '' => 'og_image_alt', + 'og:image:height' => 'og_image_height', + 'og:image:secure_url' => 'og_image_secure_url', + 'og:image:type' => 'og_image_type', + 'og:image:url' => 'og_image_url', + 'og:image:width' => 'og_image_width', + 'og:latitude' => 'og_latitude', + 'og:locale' => 'og_locale', + 'og:locale:alternate' => 'og_locale_alternative', + 'og:locality' => 'og_locality', + 'og:longitude' => 'og_longitude', + 'og:phone_number' => 'og_phone_number', + 'og:postal_code' => 'og_postal_code', + 'og:region' => 'og_region', + 'og:see_also' => 'og_see_also', + 'og:site_name' => 'og_site_name', + 'og:street_address' => 'og_street_address', + 'og:title' => 'og_title', + 'og:type' => 'og_type', + 'og:updated_time' => 'og_updated_time', + 'og:url' => 'og_url', + // @todo '' => 'og_video', + 'og:video:height' => 'og_video_height', + 'og:video:secure_url' => 'og_video_secure_url', + 'og:video:type' => 'og_video_type', + 'og:video:url' => 'og_video_url', + 'og:video:width' => 'og_video_width', + // @todo 'profile:first_name' => '', + // @todo 'profile:gender' => '', + // @todo 'profile:last_name' => '', + // @todo 'profile:username' => '', + // @todo 'video:actor' => '', + // @todo 'video:actor:role' => '', + // @todo 'video:director' => '', + // @todo 'video:duration' => '', + // @todo 'video:release_date' => '', + // @todo 'video:series' => '', + // @todo 'video:tag' => '', + // @todo 'video:writer' => '', + // From metatag_opengraph_products.metatag.inc: + 'product:price:amount' => 'product_price_amount', + 'product:price:currency' => 'product_price_currency', + // @todo 'product:availability' => '', + // @todo 'product:brand' => '', + // @todo 'product:upc' => '', + // @todo 'product:ean' => '', + // @todo 'product:isbn' => '', + // @todo 'product:plural_title' => '', + // @todo 'product:retailer' => '', + // @todo 'product:retailer_title' => '', + // @todo 'product:retailer_part_no' => '', + // @todo 'product:mfr_part_no' => '', + // @todo 'product:size' => '', + // @todo 'product:product_link' => '', + // @todo 'product:category' => '', + // @todo 'product:color' => '', + // @todo 'product:material' => '', + // @todo 'product:pattern' => '', + // @todo 'product:shipping_cost:amount' => '', + // @todo 'product:shipping_cost:currency' => '', + // @todo 'product:weight:value' => '', + // @todo 'product:weight:units' => '', + // @todo 'product:shipping_weight:value' => '', + // @todo 'product:shipping_weight:units' => '', + // @todo 'product:expiration_time' => '', + // @todo 'product:condition' => '', + // Pinterest. + // @todo '' => 'pinterest_id', + // @todo '' => 'pinterest_description', + // @todo '' => 'pinterest_nohover', + // @todo '' => 'pinterest_url', + // @todo '' => 'pinterest_media', + // @todo '' => 'pinterest_nopin', + // @todo '' => 'pinterest_nosearch', + // From metatag_twitter_cards.metatag.inc: + 'twitter:app:country' => 'twitter_cards_app_store_country', + 'twitter:app:id:googleplay' => 'twitter_cards_app_id_googleplay', + 'twitter:app:id:ipad' => 'twitter_cards_app_id_ipad', + 'twitter:app:id:iphone' => 'twitter_cards_app_id_iphone', + 'twitter:app:name:googleplay' => 'twitter_cards_app_name_googleplay', + 'twitter:app:name:ipad' => 'twitter_cards_app_name_ipad', + 'twitter:app:name:iphone' => 'twitter_cards_app_name_iphone', + 'twitter:app:url:googleplay' => 'twitter_cards_app_url_googleplay', + 'twitter:app:url:ipad' => 'twitter_cards_app_url_ipad', + 'twitter:app:url:iphone' => 'twitter_cards_app_url_iphone', + 'twitter:card' => 'twitter_cards_type', + 'twitter:creator' => 'twitter_cards_creator', + 'twitter:creator:id' => 'twitter_cards_creator_id', + 'twitter:data1' => 'twitter_cards_data1', + 'twitter:data2' => 'twitter_cards_data2', + 'twitter:description' => 'twitter_cards_description', + 'twitter:dnt' => 'twitter_cards_donottrack', + 'twitter:image' => 'twitter_cards_image', + 'twitter:image0' => 'twitter_cards_gallery_image0', + 'twitter:image1' => 'twitter_cards_gallery_image1', + 'twitter:image2' => 'twitter_cards_gallery_image2', + 'twitter:image3' => 'twitter_cards_gallery_image3', + 'twitter:image:alt' => 'twitter_cards_image_alt', + 'twitter:image:height' => 'twitter_cards_image_height', + 'twitter:image:width' => 'twitter_cards_image_width', + 'twitter:label1' => 'twitter_cards_label1', + 'twitter:label2' => 'twitter_cards_label2', + 'twitter:player' => 'twitter_cards_player', + 'twitter:player:height' => 'twitter_cards_player_height', + 'twitter:player:stream' => 'twitter_cards_player_stream', + 'twitter:player:stream:content_type' => 'twitter_cards_player_stream_content_type', + 'twitter:player:width' => 'twitter_cards_player_width', + 'twitter:site' => 'twitter_cards_site', + 'twitter:site:id' => 'twitter_cards_site_id', + 'twitter:title' => 'twitter_cards_title', + 'twitter:url' => 'twitter_cards_page_url', + // From metatag_verification.metatag.inc: + 'baidu-site-verification' => 'baidu', + 'google-site-verification' => 'bing', + 'msvalidate.01' => 'google', + 'norton-safeweb-site-verification' => 'norton_safe_web', + 'p:domain_verify' => 'pinterest', + // @todo '' => 'pocket', + 'yandex-verification' => 'yandex', + ]; + + // Trigger hook_metatag_migrate_tags_map_alter(). + // Allow modules to override tags or the entity used for token replacements. + \Drupal::service('module_handler')->alter('metatag_migrate_tags_map', $map); + + return $map; + } + +} diff --git a/src/Plugin/migrate/source/d7/MetatagField.php b/src/Plugin/migrate/source/d7/MetatagField.php new file mode 100644 index 0000000..305f239 --- /dev/null +++ b/src/Plugin/migrate/source/d7/MetatagField.php @@ -0,0 +1,42 @@ +select('metatag', 'm') + ->fields('m', ['entity_type']) + ->groupBy('entity_type'); + } + + /** + * {@inheritdoc} + */ + public function fields() { + $fields = ['entity_type' => $this->t('Entity type')]; + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['entity_type']['type'] = 'string'; + return $ids; + } + +} diff --git a/src/Plugin/migrate/source/d7/MetatagFieldInstance.php b/src/Plugin/migrate/source/d7/MetatagFieldInstance.php new file mode 100644 index 0000000..8b4f2eb --- /dev/null +++ b/src/Plugin/migrate/source/d7/MetatagFieldInstance.php @@ -0,0 +1,94 @@ +entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('state'), + $container->get('entity.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + public function query() { + return $this->select('metatag', 'm') + ->fields('m', ['entity_type']) + ->groupBy('entity_type'); + } + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'entity_type' => $this->t('Entity type'), + 'bundle' => $this->t('Bundle'), + ]; + } + + /** + * Returns each entity_type/bundle pair. + */ + public function initializeIterator() { + $bundles = []; + foreach (parent::initializeIterator() as $instance) { + $bundle_info = $this->entityTypeBundleInfo + ->getBundleInfo($instance['entity_type']); + foreach (array_keys($bundle_info) as $bundle) { + $bundles[] = [ + 'entity_type' => $instance['entity_type'], + 'bundle' => $bundle, + ]; + } + } + return new \ArrayIterator($bundles); + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['entity_type']['type'] = 'string'; + $ids['bundle']['type'] = 'string'; + return $ids; + } + +} diff --git a/tests/fixtures/drupal7.php b/tests/fixtures/drupal7.php new file mode 100644 index 0000000..bf0ec7f --- /dev/null +++ b/tests/fixtures/drupal7.php @@ -0,0 +1,229 @@ +schema()->createTable('metatag', [ + 'fields' => [ + 'entity_type' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ], + 'entity_id' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + 'unsigned' => TRUE, + ], + 'revision_id' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + 'unsigned' => TRUE, + ], + 'language' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ], + 'data' => [ + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ], + ], + 'primary key' => [ + 'entity_type', + 'entity_id', + 'revision_id', + 'language', + ], + 'indexes' => [ + 'type_revision' => [ + 'entity_type', + 'revision_id', + ], + ], + 'mysql_character_set' => 'utf8', +]); + +$connection->insert('metatag') + ->fields([ + 'entity_type', + 'entity_id', + 'revision_id', + 'language', + 'data', + ]) + ->values([ + 'entity_type' => 'node', + 'entity_id' => '998', + 'revision_id' => '998', + 'language' => 'und', + 'data' => serialize([ + // A very basic meta tag. + 'keywords' => ['value' => 'keynoderevision'], + // A meta tag that changed its tag name in D8. + 'canonical' => ['value' => 'the-node'], + ]), + ]) + ->values([ + 'entity_type' => 'node', + 'entity_id' => '998', + 'revision_id' => '999', + 'language' => 'und', + 'data' => serialize([ + 'keywords' => ['value' => 'keynode'], + 'canonical' => ['value' => 'the-node'], + ]), + ]) + ->values([ + 'entity_type' => 'user', + 'entity_id' => '2', + 'revision_id' => '0', + 'language' => 'und', + 'data' => serialize([ + 'keywords' => ['value' => 'keyuser'], + 'canonical' => ['value' => 'the-node'], + ]), + ]) + ->values([ + 'entity_type' => 'taxonomy_term', + 'entity_id' => '152', + 'revision_id' => '0', + 'language' => 'und', + 'data' => serialize([ + 'keywords' => ['value' => 'keytaxonomy2'], + 'canonical' => ['value' => 'the-term'], + ]), + ]) + ->execute(); + +$connection->insert('node') + ->fields([ + 'nid', + 'vid', + 'type', + 'language', + 'title', + 'uid', + 'status', + 'created', + 'changed', + 'comment', + 'promote', + 'sticky', + 'tnid', + 'translate', + ]) + ->values([ + 'nid' => '998', + 'vid' => '999', + 'type' => 'test_content_type', + 'language' => 'en', + 'title' => 'An Edited Node', + 'uid' => '2', + 'status' => '1', + 'created' => '1421727515', + 'changed' => '1441032132', + 'comment' => '2', + 'promote' => '1', + 'sticky' => '0', + 'tnid' => '0', + 'translate' => '0', + ]) + ->execute(); + +$connection->insert('node_revision') + ->fields([ + 'nid', + 'vid', + 'uid', + 'title', + 'log', + 'timestamp', + 'status', + 'comment', + 'promote', + 'sticky', + ]) + ->values([ + 'nid' => '998', + 'vid' => '998', + 'uid' => '1', + 'title' => 'A Node', + 'log' => '', + 'timestamp' => '1441032131', + 'status' => '1', + 'comment' => '2', + 'promote' => '1', + 'sticky' => '0', + ]) + ->values([ + 'nid' => '998', + 'vid' => '999', + 'uid' => '1', + 'title' => 'An Edited Node', + 'log' => '', + 'timestamp' => '1441032132', + 'status' => '1', + 'comment' => '2', + 'promote' => '1', + 'sticky' => '0', + ]) + ->execute(); + +$connection->insert('taxonomy_term_data') + ->fields([ + 'tid', + 'vid', + 'name', + 'description', + 'format', + 'weight', + ]) + ->values([ + '152', + '1', + 'A Term', + '', + 'plain_text', + '0', + ]) + ->execute(); + +$connection->insert('system') + ->fields([ + 'filename', + 'name', + 'type', + 'owner', + 'status', + 'bootstrap', + 'schema_version', + 'weight', + 'info', + ]) + ->values([ + 'filename' => 'sites/all/modules/metatag/metatag.module', + 'name' => 'metatag', + 'type' => 'module', + 'owner' => '', + 'status' => '1', + 'bootstrap' => '0', + 'schema_version' => '7115', + 'weight' => '0', + 'info' => 'a:12:{s:4:"name";s:7:"Metatag";s:11:"description";s:47:"Adds support and an API to implement meta tags.";s:7:"package";s:3:"SEO";s:4:"core";s:3:"7.x";s:12:"dependencies";a:3:{i:0;s:23:"drupal:system (>= 7.40)";i:1;s:13:"ctools:ctools";i:2;s:11:"token:token";}s:9:"configure";s:28:"admin/config/search/metatags";s:5:"files";a:30:{i:0;s:11:"metatag.inc";i:1;s:19:"metatag.migrate.inc";i:2;s:22:"metatag.search_api.inc";i:3;s:25:"tests/metatag.helper.test";i:4;s:23:"tests/metatag.unit.test";i:5;s:30:"tests/metatag.tags_helper.test";i:6;s:23:"tests/metatag.tags.test";i:7;s:23:"tests/metatag.node.test";i:8;s:23:"tests/metatag.term.test";i:9;s:23:"tests/metatag.user.test";i:10;s:35:"tests/metatag.core_tag_removal.test";i:11;s:30:"tests/metatag.bulk_revert.test";i:12;s:34:"tests/metatag.string_handling.test";i:13;s:44:"tests/metatag.string_handling_with_i18n.test";i:14;s:22:"tests/metatag.xss.test";i:15;s:33:"tests/metatag.output_caching.test";i:16;s:24:"tests/metatag.image.test";i:17;s:25:"tests/metatag.locale.test";i:18;s:33:"tests/metatag.node.with_i18n.test";i:19;s:33:"tests/metatag.term.with_i18n.test";i:20;s:35:"tests/metatag.with_i18n_output.test";i:21;s:37:"tests/metatag.with_i18n_disabled.test";i:22;s:35:"tests/metatag.with_i18n_config.test";i:23;s:26:"tests/metatag.with_me.test";i:24;s:29:"tests/metatag.with_media.test";i:25;s:30:"tests/metatag.with_panels.test";i:26;s:32:"tests/metatag.with_profile2.test";i:27;s:34:"tests/metatag.with_search_api.test";i:28;s:44:"tests/metatag.with_workbench_moderation.test";i:29;s:29:"tests/metatag.with_views.test";}s:17:"test_dependencies";a:14:{i:0;s:11:"devel:devel";i:1;s:33:"imagecache_token:imagecache_token";i:2;s:37:"entity_translation:entity_translation";i:3;s:9:"i18n:i18n";i:4;s:5:"me:me";i:5;s:23:"file_entity:file_entity";i:6;s:27:"media:media (>= 2.0, < 3.0)";i:7;s:13:"panels:panels";i:8;s:17:"profile2:profile2";i:9;s:13:"entity:entity";i:10;s:21:"search_api:search_api";i:11;s:41:"workbench_moderation:workbench_moderation";i:12;s:11:"views:views";i:13;s:15:"context:context";}s:5:"mtime";i:1550007449;s:7:"version";N;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', + ]) + ->execute(); diff --git a/tests/src/Kernel/Migrate/d7/MigrateMetatagTest.php b/tests/src/Kernel/Migrate/d7/MigrateMetatagTest.php new file mode 100644 index 0000000..5a117b7 --- /dev/null +++ b/tests/src/Kernel/Migrate/d7/MigrateMetatagTest.php @@ -0,0 +1,134 @@ +installSchema('file', ['file_usage']); + $this->installEntitySchema('file'); + $this->container->get('stream_wrapper_manager') + ->registerWrapper('public', PublicStream::class, StreamWrapperInterface::NORMAL); + + $fs = \Drupal::service('file_system'); + // The public file directory active during the test will serve as the + // root of the fictional Drupal 7 site we're migrating. + $fs->mkdir('public://sites/default/files', NULL, TRUE); + file_put_contents('public://sites/default/files/cube.jpeg', str_repeat('*', 3620)); + + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = $this->getMigration('d7_file'); + // Set the source plugin's source_base_path configuration value, which + // would normally be set by the user running the migration. + $source = $migration->getSourceConfiguration(); + $source['constants']['source_base_path'] = $fs->realpath('public://'); + $migration->set('source', $source); + $this->executeMigration($migration); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->loadFixture(__DIR__ . '/../../../../fixtures/drupal7.php'); + + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('taxonomy_term'); + $this->installConfig(static::$modules); + $this->installSchema('node', ['node_access']); + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('metatag_defaults'); + + $this->executeMigrations([ + 'd7_metatag_field', + 'd7_node_type', + 'd7_taxonomy_vocabulary', + 'd7_metatag_field_instance', + 'd7_metatag_field_instance_widget_settings', + 'd7_user_role', + 'd7_user', + 'd7_comment_type', + 'd7_field', + 'd7_field_instance', + ]); + $this->fileMigrationSetup(); + $this->executeMigrations([ + 'd7_node:test_content_type', + 'd7_node:article', + 'd7_node:forum', + 'd7_node:blog', + 'd7_node_revision:test_content_type', + 'd7_taxonomy_term', + ]); + } + + /** + * Test Metatag migration from Drupal 7 to 8. + */ + public function testMetatag() { + /** @var \Drupal\node\Entity\Node $node */ + $node = Node::load(998); + $this->assertTrue($node instanceof NodeInterface); + $this->assertTrue($node->hasField('field_metatag')); + $this->assertSame('a:2:{s:8:"keywords";a:1:{s:5:"value";s:7:"keynode";}s:13:"canonical_url";a:1:{s:5:"value";s:8:"the-node";}}', $node->field_metatag->value); + + $node = node_revision_load(998); + $this->assertTrue($node instanceof NodeInterface); + $this->assertTrue($node->hasField('field_metatag')); + $this->assertSame('a:2:{s:8:"keywords";a:1:{s:5:"value";s:15:"keynoderevision";}s:13:"canonical_url";a:1:{s:5:"value";s:8:"the-node";}}', $node->field_metatag->value); + + /** @var \Drupal\user\Entity\User $user */ + $user = User::load(2); + $this->assertTrue($user instanceof UserInterface); + $this->assertTrue($user->hasField('field_metatag')); + $this->assertSame('a:2:{s:8:"keywords";a:1:{s:5:"value";s:7:"keyuser";}s:13:"canonical_url";a:1:{s:5:"value";s:8:"the-node";}}', $user->field_metatag->value); + + /** @var \Drupal\taxonomy\Entity\Term $term */ + $term = Term::load(152); + $this->assertTrue($term instanceof TermInterface); + $this->assertTrue($term->hasField('field_metatag')); + $this->assertSame('a:2:{s:8:"keywords";a:1:{s:5:"value";s:12:"keytaxonomy2";}s:13:"canonical_url";a:1:{s:5:"value";s:8:"the-term";}}', $term->field_metatag->value); + } + +} diff --git a/tests/src/Unit/Migrate/d7/MetatagFieldInstanceTest.php b/tests/src/Unit/Migrate/d7/MetatagFieldInstanceTest.php new file mode 100644 index 0000000..760e70e --- /dev/null +++ b/tests/src/Unit/Migrate/d7/MetatagFieldInstanceTest.php @@ -0,0 +1,87 @@ + 'test', + 'source' => [ + 'plugin' => 'd7_metatag_field_instance', + ], + ]; + + protected $expectedResults = [ + [ + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + ], + [ + 'entity_type' => 'taxonomy_term', + 'bundle' => 'test_vocabulary', + ], + [ + 'entity_type' => 'user', + 'bundle' => 'user', + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->databaseContents['metatag'] = $this->expectedResults; + + $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $state = $this->getMock('Drupal\Core\State\StateInterface'); + $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $entity_type_bundle_info = $this->getMockBuilder('Drupal\Core\Entity\EntityTypeBundleInfo') + ->disableOriginalConstructor() + ->getMock(); + $entity_type_bundle_info->expects($this->any()) + ->method('getBundleInfo') + ->willReturnMap([ + ['node', ['test_content_type' => 'test_content_type']], + ['taxonomy_term', ['test_vocabulary' => 'test_vocabulary']], + ['user', ['user' => 'user']], + ]); + + $migration = $this->getMigration(); + // @todo Replace this. + // $migration->expects($this->any()) + // ->method('getHighWater') + // ->will($this->returnValue(static::ORIGINAL_HIGH_WATER)); + + // Setup the plugin. + $plugin_class = static::PLUGIN_CLASS; + $plugin = new $plugin_class($this->migrationConfiguration['source'], $this->migrationConfiguration['source']['plugin'], [], $migration, $state, $entity_manager, $entity_type_bundle_info); + + // Do some reflection to set the database and moduleHandler. + $plugin_reflection = new \ReflectionClass($plugin); + $database_property = $plugin_reflection->getProperty('database'); + $database_property->setAccessible(TRUE); + $module_handler_property = $plugin_reflection->getProperty('moduleHandler'); + $module_handler_property->setAccessible(TRUE); + + // Set the database and the module handler onto our plugin. + $database_property->setValue($plugin, $this->getDatabase($this->databaseContents + ['test_map' => []])); + $module_handler_property->setValue($plugin, $module_handler); + + $plugin->setStringTranslation($this->getStringTranslationStub()); + $migration->expects($this->any()) + ->method('getSourcePlugin') + ->will($this->returnValue($plugin)); + $this->source = $plugin; + $this->expectedCount = count($this->expectedResults); + } + +} diff --git a/tests/src/Unit/Migrate/d7/MetatagFieldTest.php b/tests/src/Unit/Migrate/d7/MetatagFieldTest.php new file mode 100644 index 0000000..2873418 --- /dev/null +++ b/tests/src/Unit/Migrate/d7/MetatagFieldTest.php @@ -0,0 +1,43 @@ + 'test', + 'source' => [ + 'plugin' => 'd7_metatag_field', + ], + ]; + + protected $expectedResults = [ + [ + 'entity_type' => 'node', + ], + [ + 'entity_type' => 'taxonomy_term', + ], + [ + 'entity_type' => 'user', + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->databaseContents['metatag'] = $this->expectedResults; + parent::setUp(); + } + +}