diff --git a/core/modules/field/migration_templates/d7_field_instance.yml b/core/modules/field/migration_templates/d7_field_instance.yml
index f3518c9..fc0b524 100644
--- a/core/modules/field/migration_templates/d7_field_instance.yml
+++ b/core/modules/field/migration_templates/d7_field_instance.yml
@@ -8,6 +8,8 @@ source:
   plugin: d7_field_instance
   constants:
     status: true
+    plain_text: plain_text
+    text_filter_warning:
 process:
   entity_type: entity_type
   field_name: field_name
@@ -29,6 +31,47 @@ process:
       - default_value
       - widget_settings
   translatable: translatable
+  warn_if_plain_text_filter_missing:
+    - plugin: static_map
+      source: type
+      default_value: null
+      map:
+        text: true
+        text_long: true
+    - plugin: skip_on_empty
+      method: process
+    - plugin: static_map
+      source: instance_settings/text_processing
+      default_value: null
+      map:
+        0: true
+    - plugin: skip_on_empty
+      method: process
+    - plugin: log
+      level: warning
+      message: >
+        Field '@field_name' on '@entity_type' entities has both formatted
+        text and unformatted text instances. The field on bundle '@bundle'
+        will be converted from unformatted text into formatted text with the
+        plain-text filter.
+    - plugin: entity_exists
+      source: constants/plain_text
+      entity_type: filter_format
+    - plugin: callback
+      callable: intval
+    - plugin: static_map
+      map:
+        0: true
+        1: false
+    - plugin: skip_on_empty
+      method: process
+    - plugin: log
+      level: warning
+      message: >
+        The plain text filter format is missing! Plain text field values may be
+        displayed without safe filtering.
+      context:
+        field: field_name
 destination:
   plugin: entity:field_config
 migration_dependencies:
diff --git a/core/modules/field/src/Plugin/migrate/source/d7/Field.php b/core/modules/field/src/Plugin/migrate/source/d7/Field.php
index d9aef91..44acad6 100644
--- a/core/modules/field/src/Plugin/migrate/source/d7/Field.php
+++ b/core/modules/field/src/Plugin/migrate/source/d7/Field.php
@@ -12,21 +12,14 @@
  *   id = "d7_field"
  * )
  */
-class Field extends DrupalSqlBase {
+class Field extends FieldBase {
 
   /**
    * {@inheritdoc}
    */
   public function query() {
-    $query = $this->select('field_config', 'fc')
-      ->distinct()
-      ->fields('fc')
-      ->fields('fci', ['entity_type'])
-      ->condition('fc.active', 1)
-      ->condition('fc.deleted', 0)
-      ->condition('fc.storage_active', 1);
-    $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id');
-
+    $query = parent::query();
+    $query->fields('fc');
     return $query;
   }
 
@@ -71,4 +64,12 @@ public function getIds() {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function instancesRows($instances) {
+    // Field bases only want one row per instance-set.
+    return [$instances[0]];
+  }
+
 }
diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldBase.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldBase.php
new file mode 100644
index 0000000..c2248d5
--- /dev/null
+++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldBase.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\field\Plugin\migrate\source\d7;
+
+use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
+
+/**
+ * Base class for Drupal 7 field sources.
+ */
+abstract class FieldBase extends DrupalSqlBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    $query = $this->select('field_config', 'fc');
+    $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id');
+    $query->fields('fc', ['type', 'field_name'])
+      ->fields('fci', ['entity_type'])
+      ->condition('fc.active', 1)
+      ->condition('fc.deleted', 0)
+      ->condition('fc.storage_active', 1);
+    $query->addField('fci', 'data', 'instance_data');
+    return $query;
+  }
+
+  /**
+   * Determine which rows to yield for each set of instances.
+   *
+   * @param array $instances
+   *   The data for the field's instances.
+   *
+   * @return array
+   *   A list of row arrays to yield.
+   */
+  abstract protected function instancesRows($instances);
+
+  /**
+   * Determine the type of a field.
+   *
+   * @param array $instances
+   *   The data for the field's instances.
+   *
+   * @return string
+   *   The field type.
+   */
+  protected function getType($instances) {
+    // Determine whether D7 text fields are all unformatted, and should
+    // therefore become 'string'.
+    $type = NULL;
+    $useString = FALSE;
+
+    foreach ($instances as $instance) {
+      if (!isset($type)) {
+        $type = $instance['type'];
+        // If this is a text field, we might want to become 'string'.
+        $useString = in_array($type, ['text', 'text_long']);
+      }
+
+      if ($useString) {
+        // If this is a formatted field, don't become 'string'.
+        $data = unserialize($instance['instance_data']);
+        if (!empty($data['settings']['text_processing'])) {
+          $useString = FALSE;
+        }
+      }
+    }
+
+    if ($useString) {
+      return str_replace('text', 'string', $type);
+    }
+    return $type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializeIterator() {
+    // Collate fields by D8 instance.
+    $instanceSets = [];
+    $results = $this->prepareQuery()->execute();
+    foreach ($results as $result) {
+      $instanceKey = $result['entity_type'] . ':' . $result['field_name'];
+      $instanceSets[$instanceKey][] = $result;
+    }
+
+    // Figure out the field type for each instance.
+    $rows = [];
+    foreach ($instanceSets as $instances) {
+      $type = $this->getType($instances);
+      foreach ($this->instancesRows($instances) as $row) {
+        $row['type'] = $type;
+        $rows[] = $row;
+      }
+    }
+    return new \ArrayIterator($rows);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count($refresh = FALSE) {
+    return $this->initializeIterator()->count();
+  }
+
+}
diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php
index d06336e..463533b 100644
--- a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php
+++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php
@@ -13,21 +13,15 @@
  *   source_provider = "field"
  * )
  */
-class FieldInstance extends DrupalSqlBase {
+class FieldInstance extends FieldBase {
 
   /**
    * {@inheritdoc}
    */
   public function query() {
-    $query = $this->select('field_config_instance', 'fci')
-      ->fields('fci')
-      ->condition('fci.deleted', 0)
-      ->condition('fc.active', 1)
-      ->condition('fc.deleted', 0)
-      ->condition('fc.storage_active', 1)
-      ->fields('fc', ['type']);
-
-    $query->innerJoin('field_config', 'fc', 'fci.field_id = fc.id');
+    $query = parent::query();
+    $query->fields('fci')
+      ->condition('fc.storage_active', 1);
     $query->addField('fc', 'data', 'field_data');
 
     // Optionally filter by entity type and bundle.
@@ -128,4 +122,11 @@ public function getIds() {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function instancesRows($instances) {
+    return $instances;
+  }
+
 }
diff --git a/core/modules/migrate/src/Plugin/migrate/process/LogMessage.php b/core/modules/migrate/src/Plugin/migrate/process/LogMessage.php
new file mode 100644
index 0000000..dd25b61
--- /dev/null
+++ b/core/modules/migrate/src/Plugin/migrate/process/LogMessage.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\migrate\Plugin\migrate\process;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\migrate\ProcessPluginBase;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\MigrateExecutableInterface;
+use Drupal\migrate\Row;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Logs a message about a migration, and then passes through the value.
+ *
+ * Available configuration keys:
+ * - level: The message severity.
+ * - message: The message, with placeholders.
+ *
+ * Examples:
+ *
+ * @code
+ * process:
+ *   show_notice:
+ *     - plugin: log
+ *       level: notice
+ *       message: 'Item imported!'
+ * @endcode
+ *
+ * @MigrateProcessPlugin(
+ *   id = "log"
+ * )
+ */
+class LogMessage extends ProcessPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+
+
+  /**
+   * EntityExists constructor.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('logger.factory')->get('migrate')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
+    // TODO: Use the current MigrateMessage?
+    // TODO: Log to the migrate message table?
+    $process = empty($this->configuration['context']) ? []
+      : $this->configuration['context'];
+    $contextRow = clone $row;
+    $migrate_executable->processRow($contextRow, $process);
+    $contextItems = $contextRow->getDestination() + $contextRow->getSource();
+    $context = [];
+    foreach ($contextItems as $key => $value) {
+      $context['@' . $key] = $value;
+    }
+    $this->logger->log(
+      $this->configuration['level'],
+      $this->configuration['message'],
+      $context
+    );
+
+    // Return original value.
+    return $value;
+  }
+
+}
diff --git a/core/modules/text/src/Plugin/migrate/cckfield/TextField.php b/core/modules/text/src/Plugin/migrate/cckfield/TextField.php
index 1d18b30..d384cd9 100644
--- a/core/modules/text/src/Plugin/migrate/cckfield/TextField.php
+++ b/core/modules/text/src/Plugin/migrate/cckfield/TextField.php
@@ -58,6 +58,18 @@ public function processCckFieldValues(MigrationInterface $migration, $field_name
       }
       $process['value']['map'][$checked_value] = 1;
     }
+    elseif (isset($field_info['instance_settings']['text_processing'])
+      && $field_info['instance_settings']['text_processing'] === 0
+    ) {
+      // This is a D7 plain text field, no formats supported.
+      $process = [
+        'value' => 'value',
+        'format' => [
+          'plugin' => 'default_value',
+          'default_value' => 'plain_text',
+        ],
+      ];
+    }
     else {
       // See \Drupal\migrate_drupal\Plugin\migrate\source\d6\User::baseFields(),
       // signature_format for an example of the YAML that represents this
diff --git a/core/modules/text/src/Plugin/migrate/field/d7/StringField.php b/core/modules/text/src/Plugin/migrate/field/d7/StringField.php
new file mode 100644
index 0000000..387a36b
--- /dev/null
+++ b/core/modules/text/src/Plugin/migrate/field/d7/StringField.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\text\Plugin\migrate\field\d7;
+
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
+
+/**
+ * @MigrateField(
+ *   id = "string_field",
+ *   core = {7},
+ *   type_map = {
+ *     "string" = "string",
+ *     "string_long" = "string_long"
+ *   }
+ * )
+ */
+class StringField extends FieldPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processFieldFormatter(MigrationInterface $migration) {
+    return [
+      'plugin' => 'static_map',
+      'source' => 'type',
+      'map' => [
+        'string' => 'string',
+        'string_long' => 'basic_string',
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldFormatterMap() {
+    // Map based on type, not D7 formatter.
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldWidgetMap() {
+    return [
+      'text_textfield' => 'string_textfield',
+      'text_textarea' => 'string_textarea',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processFieldValues(MigrationInterface $migration, $field_name, $data) {
+    $process = [
+      'plugin' => 'iterator',
+      'source' => $field_name,
+      'process' => [
+        'value' => 'value',
+      ],
+    ];
+    $migration->mergeProcessOfProperty($field_name, $process);
+  }
+
+}
diff --git a/core/modules/text/tests/src/Unit/Migrate/TextFieldTest.php b/core/modules/text/tests/src/Unit/Migrate/TextFieldTest.php
index 7f703b7..9317b4a 100644
--- a/core/modules/text/tests/src/Unit/Migrate/TextFieldTest.php
+++ b/core/modules/text/tests/src/Unit/Migrate/TextFieldTest.php
@@ -71,6 +71,30 @@ public function testProcessFilteredTextFieldValues() {
   /**
    * @covers ::processCckFieldValues
    */
+  public function testProcessUnfilteredTextFieldValues() {
+    $field_info = [
+      'widget_type' => 'text_textfield',
+      'instance_settings' => [
+        'text_processing' => 0,
+      ],
+    ];
+    $this->plugin->processCckFieldValues($this->migration, 'field_test', $field_info);
+
+    $process = $this->migration->getProcess();
+    $this->assertSame('iterator', $process['plugin']);
+    $this->assertSame('field_test', $process['source']);
+
+    // Ensure unfiltered D7 text fields are marked as plain text.
+    $this->assertSame('value', $process['process']['value']);
+    $this->assertSame([
+      'plugin' => 'default_value',
+      'default_value' => 'plain_text',
+    ], $process['process']['format']);
+  }
+
+  /**
+   * @covers ::processCckFieldValues
+   */
   public function testProcessBooleanTextImplicitValues() {
     $info = [
       'widget_type' => 'optionwidgets_onoff',
