diff --git a/migrate.info b/migrate.info
index face2c9..a90e047 100644
--- a/migrate.info
+++ b/migrate.info
@@ -37,6 +37,7 @@ files[] = plugins/sources/sql.inc
 files[] = plugins/sources/sqlmap.inc
 files[] = plugins/sources/mssql.inc
 files[] = plugins/sources/oracle.inc
+files[] = plugins/sources/spreadsheet.inc
 files[] = plugins/sources/xml.inc
 files[] = tests/import/options.test
 files[] = tests/plugins/destinations/comment.test
diff --git a/migrate_example/migrate_example.info b/migrate_example/migrate_example.info
index 0b0f064..35cd03e 100644
--- a/migrate_example/migrate_example.info
+++ b/migrate_example/migrate_example.info
@@ -8,6 +8,7 @@ dependencies[] = comment
 dependencies[] = migrate
 dependencies[] = list
 dependencies[] = number
+dependencies[] = phpexcel
 
 ;node_reference is useful but not required
 ;dependencies[] = node_reference
@@ -15,6 +16,7 @@ dependencies[] = number
 files[] = migrate_example.module
 files[] = beer.inc
 files[] = wine.inc
+files[] = spreadsheet.inc
 
 ; For testing table_copy plugin. Since is infrequently used, we comment it out.
 ; files[] = example.table_copy.inc
diff --git a/migrate_example/migrate_example.migrate.inc b/migrate_example/migrate_example.migrate.inc
index 6d509ea..f050ee9 100644
--- a/migrate_example/migrate_example.migrate.inc
+++ b/migrate_example/migrate_example.migrate.inc
@@ -42,6 +42,8 @@ function migrate_example_migrate_api() {
       'WineCommentUpdates' => array('class_name' => 'WineCommentUpdatesMigration'),
       'WineVarietyUpdates' => array('class_name' => 'WineVarietyUpdatesMigration'),
       'WineUserUpdates' => array('class_name' => 'WineUserUpdatesMigration'),
+      'SpreadsheetBasicPages' => array('class_name' => 'SpreadsheetBasicPagesMigration'),
     ),
   );
   return $api;
diff --git a/migrate_example/spreadsheet.inc b/migrate_example/spreadsheet.inc
new file mode 100644
index 0000000..156aeed
--- /dev/null
+++ b/migrate_example/spreadsheet.inc
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Use the Migrate module to import example basic page content.
+ *
+ * We will use the Migrate API to insert this data into Drupal.
+ */
+
+/**
+ * Define the basic page migration.
+ */
+class SpreadsheetBasicPagesMigration extends DynamicMigration {
+  public function __construct() {
+    parent::__construct(MigrateGroup::getInstance('xls', array('default')));
+    $this->description = t('Testing /plugins/sources/spreadsheet.inc plugin.');
+    $columns = array(
+      array('title', 'Title'),
+      array('body', 'Body'),
+    );
+    // Define our source, specifying the columns above
+    $this->source = new MigrateSourceSpreadsheet(drupal_get_path('module', 'migrate_example') . '/spreadsheet/SpreadsheetBasicPages.xls', $columns);
+    $node_options = MigrateDestinationNode::options(LANGUAGE_NONE, 'full_html');
+    $this->destination = new MigrateDestinationNode('page', $node_options);
+    $this->map = new MigrateSQLMap($this->machineName,
+      array(
+        'title' => array(
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+        ),
+      ),
+      MigrateDestinationNode::getKeySchema()
+    );
+    // Map the node's fields (destination, source)
+    $this->addFieldMapping('uid')
+      ->defaultValue(1);
+    $this->addFieldMapping('title', 'title');
+    $this->addFieldMapping('body', 'body');
+  }
+}
diff --git a/migrate_example/spreadsheet/SpreadsheetBasicPages.xls b/migrate_example/spreadsheet/SpreadsheetBasicPages.xls
new file mode 100644
index 0000000..707c93c
Binary files /dev/null and b/migrate_example/spreadsheet/SpreadsheetBasicPages.xls differ
diff --git a/plugins/sources/spreadsheet.inc b/plugins/sources/spreadsheet.inc
new file mode 100644
index 0000000..d25692f
--- /dev/null
+++ b/plugins/sources/spreadsheet.inc
@@ -0,0 +1,220 @@
+<?php
+
+/**
+ * @file
+ * Define a MigrateSource for importing from spreadsheet files.
+ *
+ * Requires the PHPExcel library to be installed.
+ * - Download PHPExcel at http://phpexcel.codeplex.com/.
+ * - Extract the archive to a temporary folder.
+ * - Ensure the hosting environment fulfills the requirements found in
+ *   install.txt.
+ * - Copy the contents of the Classes folder to an appropriate location
+ *   (sites/all/libraries/PHPExcel).
+ */
+
+/**
+ * Implements MigrateSource, to handle imports from XLS files.
+ */
+class MigrateSourceSpreadsheet extends MigrateSource {
+  /**
+   * PHPExcel object for storing the workbook data.
+   *
+   * @var PHPExcel
+   */
+  protected $workbook;
+
+  /**
+   * The name of the worksheet that will be processed.
+   *
+   * @var string
+   */
+  protected $sheetName;
+
+  /**
+   * Number of rows in the worksheet that is being processed.
+   *
+   * @var integer
+   */
+  protected $rows = 0;
+
+  /**
+   * Number of columns in the worksheet that is being processed.
+   *
+   * @var integer
+   */
+  protected $cols = 0;
+
+  /**
+   * List of available source fields.
+   *
+   * @var array
+   */
+  protected $fields = array();
+
+  /**
+   * The current row number in the XLS file.
+   *
+   * @var integer
+   */
+  protected $rowNumber;
+
+  /**
+   * Simple initialization.
+   *
+   * @param string $path
+   *   The path to the source file.
+   * @param string $sheet_name
+   *   The name of the sheet to be processed.
+   * @param array $options
+   *   Options applied to this source.
+   */
+  public function __construct($path, $sheet_name, array $options = array()) {
+    parent::__construct($options);
+    $this->file = $path;
+    $this->sheetName = $sheet_name;
+
+    // Load the workbook.
+    if ($this->load()) {
+      $worksheet = $this->workbook->getSheet();
+
+      // Get the dimensions of the worksheet.
+      $this->rows = $worksheet->getHighestDataRow();
+      $this->cols = PHPExcel_Cell::columnIndexFromString($worksheet->getHighestDataColumn());
+
+      // Map field names to their column index.
+      for ($col = 0; $col < $this->cols; ++$col) {
+        $this->fields[$col] = trim($worksheet->getCellByColumnAndRow($col, 1)->getValue());
+      }
+
+      $this->unload();
+    }
+  }
+
+  /**
+   * Loads the workbook.
+   *
+   * @return bool
+   *   Returns true if the workbook was successfully loaded, otherwise false.
+   */
+  public function load() {
+    // Check that the file exists.
+    if (!file_exists($this->file)) {
+      Migration::displayMessage(t('The file !filename does not exist.', array('!filename' => $this->file)));
+      return FALSE;
+    }
+
+    if (module_exists('libraries') && module_exists('phpexcel')) {
+      $library = libraries_load('PHPExcel');
+      if (empty($library['loaded'])) {
+        Migration::displayMessage(t('The PHPExcel library could not be found.'));
+        return FALSE;
+      }
+    }
+    else {
+      Migration::displayMessage(t('The Libraries API module is not enabled.'));
+      return FALSE;
+    }
+
+    // Load the workbook.
+    try {
+      // Identify the type of the input file.
+      $type = PHPExcel_IOFactory::identify($this->file);
+      // Create a new Reader of the file type.
+      $reader = PHPExcel_IOFactory::createReader($type);
+      // Advise the Reader that we only want to load cell data.
+      $reader->setReadDataOnly(TRUE);
+      // Advise the Reader of which worksheet we want to load.
+      $reader->setLoadSheetsOnly($this->sheetName);
+      // Load the source file.
+      $this->workbook = $reader->load($this->file);
+    }
+    catch (Exception $e) {
+      Migration::displayMessage(t('Error loading file: %message', array('%message' => $e->getMessage())));
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Unloads the workbook.
+   */
+  public function unload() {
+    $this->workbook->disconnectWorksheets();
+    unset($this->workbook);
+  }
+
+  /**
+   * Returns a string representing the source query.
+   *
+   * @return string
+   */
+  public function __toString() {
+    return $this->file;
+  }
+
+  /**
+   * Returns a list of fields available to be mapped from the source query.
+   *
+   * @return array
+   *   Keys: machine names of the fields (to be passed to addFieldMapping).
+   *   Values: Human-friendly descriptions of the fields.
+   */
+  public function fields() {
+    $fields = array();
+
+    foreach ($this->fields as $name) {
+      $fields[$name] = $name;
+    }
+
+    return $fields;
+  }
+
+  /**
+   * Returns a count of all available source records.
+   */
+  public function computeCount() {
+    // Subtract 1 for the header.
+    return $this->rows - 1;
+  }
+
+  /**
+   * Implements MigrateSource::performRewind().
+   *
+   * @return void
+   */
+  public function performRewind() {
+    // Initialize the workbook if it isn't already.
+    if (!isset($this->workbook)) {
+      $this->load();
+    }
+
+    $this->rowNumber = 1;
+  }
+
+  /**
+   * Implements MigrateSource::getNextRow().
+   *
+   * @return null|object
+   */
+  public function getNextRow() {
+    ++$this->rowNumber;
+
+    if ($this->rowNumber <= $this->rows) {
+      $worksheet = $this->workbook->getSheet();
+      $row_values = array();
+
+      for ($col = 0; $col < $this->cols; ++$col) {
+        $row_values[$this->fields[$col]] = trim($worksheet->getCellByColumnAndRow($col, $this->rowNumber)->getValue());
+      }
+
+      return (object) $row_values;
+    }
+    else {
+      // EOF, close the workbook.
+      $this->unload();
+      return NULL;
+    }
+  }
+}
