diff --git a/d5/d5.inc b/d5/d5.inc
new file mode 100644
index 0000000..6295e4b
--- /dev/null
+++ b/d5/d5.inc
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * Drupal 5 implementations of functions shared among multiple types of objects.
+ */
+class DrupalVersion5 implements DrupalVersionInterface {
+  protected $arguments;
+
+  public function __construct($arguments) {
+    $this->arguments = $arguments;
+  }
+
+  /**
+   * Generate default format mappings based on matching names. E.g., if the
+   * Drupal 5 database has format 5 with name 'Filtered HTML', and the Drupal 7
+   * databas has format filtered_html with name 'Filtered HTML', the resulting
+   * array will contain the row '5' => 'filtered_html'.
+   */
+  public function getDefaultFormatMappings() {
+    migrate_instrument_start('DrupalVersion5::getDefaultFormatMappings');
+    $format_mappings = array();
+    $result = Database::getConnection('default', $this->arguments['source_connection'])
+              ->select('filter_formats', 'f')
+              ->fields('f', array('format', 'name'))
+              ->execute();
+    foreach ($result as $format_row) {
+      $format = db_select('filter_format', 'f')
+                ->fields('f', array('format'))
+                ->condition('name', $format_row->name)
+                ->execute()
+                ->fetchField();
+      if ($format) {
+        $format_mappings[$format_row->format] = $format;
+      }
+    }
+    migrate_instrument_stop('DrupalVersion5::getDefaultFormatMappings');
+    return $format_mappings;
+  }
+
+  /**
+   * Given a source path (e.g, 'node/123'), return the first alias for that path.
+   *
+   * @param $source
+   * @return string
+   */
+  public function getPath($source) {
+    migrate_instrument_start('DrupalVersion5::getPath');
+    $path = Database::getConnection('default', $this->arguments['source_connection'])
+                  ->select('url_alias', 'ua')
+                  ->fields('ua', array('dst'))
+                  ->condition('src', $source)
+                  ->execute()
+                  ->fetchField();
+    migrate_instrument_stop('DrupalVersion5::getPath');
+    return $path;
+  }
+}
diff --git a/d5/node.inc b/d5/node.inc
new file mode 100644
index 0000000..ca73be1
--- /dev/null
+++ b/d5/node.inc
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * Handling specific to a Drupal 5 source for nodes.
+ */
+class DrupalNode5Migration extends DrupalNodeMigration {
+  /**
+   * @param array $arguments
+   */
+  public function __construct(array $arguments) {
+    parent::__construct($arguments);
+
+    // Basic CCK field suffixes
+    $this->fieldSuffixes = array(
+      'computed' => 'value',
+      'date' => 'value',
+      'datestamp' => 'value',
+      'datetime' => 'value',
+      'number_decimal' => 'value',
+      'number_integer' => 'value',
+      'text' => 'value',
+      'filefield' => 'fid', // @todo: Handle list and data suffixes
+      'nodereference' => 'nid',
+    );
+  }
+
+  /**
+   * Query for basic node fields from Drupal 5.
+   *
+   * @return QueryConditionInterface
+   */
+  protected function nodeQuery() {
+    $query = Database::getConnection('default', $this->sourceConnection)
+             ->select('node', 'n')
+             ->fields('n', array('nid', 'vid', 'title', 'uid',
+               'status', 'created', 'changed', 'comment', 'promote', 'moderate',
+               'sticky'))
+             ->condition('type', $this->sourceType);
+    $query->innerJoin('node_revisions', 'nr', 'n.vid=nr.vid');
+    $query->fields('nr', array('body', 'teaser', 'format'));
+    return $query;
+  }
+
+  /**
+   * Retrieves the available fields for this content type from Drupal 5.
+   *
+   * @return array
+   */
+  public function sourceFieldList() {
+    migrate_instrument_start('DrupalNode5Migration::sourceFieldList');
+    $fields = array();
+    $this->sourceFieldTypes = array();
+
+    // Get each CCK field attached to this type.
+    $query = Database::getConnection('default', $this->sourceConnection)
+             ->select('node_field_instance', 'i')
+             ->fields('i', array('label'))
+             ->condition('i.type_name', $this->sourceType);
+    $query->innerJoin('node_field', 'f', 'i.field_name = f.field_name');
+    $query->fields('f', array('field_name', 'type'));
+    $result = $query->execute();
+    foreach ($result as $row) {
+      $fields[trim($row->field_name)] = t('!label (!type)',
+        array('!label' => $row->label, '!type' => $row->type));
+      $this->sourceFieldTypes[$row->field_name] = $row->type;
+    }
+
+    // Get each vocabulary attached to this type.
+    $query = Database::getConnection('default', $this->sourceConnection)
+             ->select('vocabulary_node_types', 'vnt')
+             ->fields('vnt', array('vid'));
+    $query->innerJoin('vocabulary', 'v', 'vnt.vid=v.vid');
+    $query->addField('v', 'name');
+    $query->condition('vnt.type', $this->sourceType);
+    $result = $query->execute();
+    foreach ($result as $row) {
+      $fields[$row->vid] = t('!label (Taxonomy Term)',
+                             array('!label' => $row->name));
+      $this->sourceFieldTypes[$row->vid] = 'taxonomy_term';
+    }
+    migrate_instrument_stop('DrupalNode5Migration::sourceFieldList');
+    return $fields;
+  }
+
+  /**
+   * Called after the basic query data is fetched - we'll use this to populate
+   * the source row with the CCK fields.
+   */
+  public function prepareRow($row) {
+    if (parent::prepareRow($row) === FALSE) {
+      return FALSE;
+    }
+
+    // Load up field data for dynamically mapped fields
+    foreach ($this->sourceFieldTypes as $field_name => $field_type) {
+      // Find the data. Simple unshared unmultiple fields will be in
+      // content_type_{$this->sourceType}. Others will be in
+      // content_$field_name.
+      // TODO: Handle multi-value fields
+      $table = "content_$field_name";
+      if (!Database::getConnection('default', $this->sourceConnection)
+          ->schema()->tableExists($table)) {
+        $table = 'content_type_' . $this->sourceType;
+      }
+      if (isset($this->fieldSuffixes[$field_type])) {
+        $suffix = $this->fieldSuffixes[$field_type];
+        $field_field = $field_name .'_'. $suffix;
+        $row->$field_name = Database::getConnection('default', $this->sourceConnection)
+                            ->select($table, 'f')
+                            ->fields('f', array($field_field))
+                            ->condition('vid', $row->vid)
+                            ->execute()
+                            ->fetchField();
+        // Allow for field-type translation
+        $method = "handle_type_$field_type";
+        if (method_exists($this, $method)) {
+          $row->$field_name = $this->$method($row->$field_name);
+        }
+        // Some fields will need translation (e.g., from a text string to a boolean)
+        $method = "handle_$field_name";
+        if (method_exists($this, $method)) {
+          $row->$field_name = $this->$method($row->$field_name);
+        }
+      }
+      else if ($field_type != 'taxonomy_term') {
+        // Only report once per field type
+        static $reported_field_types = array();
+        if (!isset($reported_field_types[$field_type])) {
+          Migration::displayMessage("No suffix for field_type=$field_type");
+          $reported_field_types[$field_type] = TRUE;
+        }
+      }
+      if (!empty($field_name) && empty($row->$field_name)) {
+        unset($row->$field_name);
+      }
+    }
+
+    // And. load up the data for taxonomy terms
+    $query = Database::getConnection('default', $this->sourceConnection)
+             ->select('term_node', 'tn')
+             ->fields('tn', array('tid'));
+    $query->innerJoin('term_data', 'td', 'tn.tid=td.tid');
+    $query->condition('td.vid', $row->vid);
+    $query->fields('td', array('vid'));
+    $result = $query->execute();
+    foreach ($result as $term_row) {
+      $row->{$term_row->vid}[] = $term_row->tid;
+    }
+  }
+}
diff --git a/d5/taxonomy.inc b/d5/taxonomy.inc
new file mode 100644
index 0000000..5b63762
--- /dev/null
+++ b/d5/taxonomy.inc
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Handling specific to a Drupal 5 source for taxonomy terms.
+ */
+class DrupalTerm5Migration extends DrupalTermMigration {
+  public function __construct(array $arguments) {
+    parent::__construct($arguments);
+
+    // Drupal 5 had no format for terms.
+    $this->addFieldMapping('format');
+  }
+
+  /**
+   * Implementation of DrupalTermMigration::termQuery().
+   *
+   * @return SelectQueryInterface
+   */
+  protected function termQuery() {
+    // Note the explode - this supports the (admittedly unusual) case of
+    // consolidating multiple vocabularies into one.
+    $query = Database::getConnection('default', $this->sourceConnection)
+             ->select('term_data', 'td')
+             ->fields('td', array('tid', 'name', 'description', 'weight'))
+             ->condition('vid', explode(',', $this->sourceVocabulary), 'IN')
+             ->orderBy('parent')
+             ->distinct();
+    // Join to the hierarchy so we can sort on parent, but we'll pull the
+    // actual parent values in separately in case there are multiples.
+    $query->leftJoin('term_hierarchy', 'th', 'td.tid=th.tid');
+    return $query;
+  }
+
+  /**
+   * Implementation of Migration::prepareRow().
+   *
+   * @param $row
+   */
+  public function prepareRow($row) {
+    if (parent::prepareRow($row) === FALSE) {
+      return FALSE;
+    }
+
+    // Add the (potentially multiple) parents
+    $result = Database::getConnection('default', $this->sourceConnection)
+              ->select('term_hierarchy', 'th')
+              ->fields('th', array('parent'))
+              ->condition('tid', $row->tid)
+              ->execute();
+    foreach ($result as $parent_row) {
+      $row->parent[] = $parent_row->parent;
+    }
+  }
+}
diff --git a/d5/user.inc b/d5/user.inc
new file mode 100644
index 0000000..c789edb
--- /dev/null
+++ b/d5/user.inc
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Handling specific to a Drupal 5 source for users.
+ */
+class DrupalUser5Migration extends DrupalUserMigration {
+  /**
+   * @param array $arguments
+   */
+  public function __construct(array $arguments) {
+    parent::__construct($arguments);
+
+    // Indicate that our incoming MD5 passwords should be rehashed for Drupal 7.
+    $this->destination = new MigrateDestinationUser(array('md5_passwords' => TRUE));
+
+    // Per-user comment settings dropped in Drupal 7.
+    $this->addUnmigratedSources(array('mode', 'threshold', 'sort'));
+
+    $this->addFieldMapping(NULL, 'timezone_name')
+         ->description('If present, assigned to D7 timezone');
+  }
+
+  /**
+   * Implementation of Migration::prepareRow().
+   *
+   * @param $row
+   */
+  public function prepareRow($row) {
+    if (parent::prepareRow($row) === FALSE) {
+      return FALSE;
+    }
+
+    /**
+     * Timezones need some runtime massaging when going from D5 to D7. Note that
+     * the date module adds a timezone_name column to the users table, so if
+     * present we can use that directly. Otherwise, we do as the D5->D7 upgrade
+     * does and just clear it - let users reset their timezones in the D7 site.
+     */
+    if (!empty($row->timezone_name)) {
+      $row->timezone = $row->timezone_name;
+    }
+    else {
+      $row->timezone = NULL;
+    }
+  }
+}
