? libraries/simplepie.inc
Index: feeds.api.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.api.php,v
retrieving revision 1.16.2.2
diff -u -p -r1.16.2.2 feeds.api.php
--- feeds.api.php	16 Nov 2010 23:13:42 -0000	1.16.2.2
+++ feeds.api.php	3 Feb 2011 13:03:52 -0000
@@ -89,37 +89,33 @@ function hook_feeds_plugins() {
 /**
  * Invoked after a feed source has been parsed, before it will be processed.
  *
- * @param $importer
- *   FeedsImporter object that has been used for importing the feed.
  * @param $source
  *  FeedsSource object that describes the source that has been imported.
+ * @param $result
+ *   FeedsParserResult object that has been parsed from the source.
  */
-function hook_feeds_after_parse(FeedsImporter $importer, FeedsSource $source) {
+function hook_feeds_after_parse(FeedsSource $source, FeedsParserResult $result) {
   // For example, set title of imported content:
-  $source->batch->title = 'Import number '. my_module_import_id();
+  $result->title = 'Import number ' . my_module_import_id();
 }
 
 /**
  * Invoked after a feed source has been imported.
  *
- * @param $importer
- *   FeedsImporter object that has been used for importing the feed.
  * @param $source
  *  FeedsSource object that describes the source that has been imported.
  */
-function hook_feeds_after_import(FeedsImporter $importer, FeedsSource $source) {
+function hook_feeds_after_import(FeedsSource $source) {
   // See geotaxonomy module's implementation for an example.
 }
 
 /**
  * Invoked after a feed source has been cleared of its items.
  *
- * @param $importer
- *   FeedsImporter object that has been used for clearing the feed.
  * @param $source
  *  FeedsSource object that describes the source that has been cleared.
  */
-function hook_feeds_after_clear(FeedsImporter $importer, FeedsSource $source) {
+function hook_feeds_after_clear(FeedsSource $source) {
 }
 
 /**
@@ -137,8 +133,6 @@ function hook_feeds_after_clear(FeedsImp
  * Use this hook to add additional mapping sources for any parser. Allows for
  * registering a callback to be invoked at mapping time.
  *
- * my_callback(FeedsImportBatch $batch, $key)
- *
  * @see my_source_get_source().
  * @see locale_feeds_parser_sources_alter().
  */
@@ -151,12 +145,14 @@ function hook_feeds_parser_sources_alter
 }
 
 /**
- * Callback specified in hook_feeds_parser_sources_alter().
+ * Example callback specified in hook_feeds_parser_sources_alter().
  *
  * To be invoked on mapping time.
  *
- * @param $batch
- *   The FeedsImportBatch object being mapped from.
+ * @param $source
+ *   The FeedsSource object being imported.
+ * @param $result
+ *   The FeedsParserResult object being mapped from.
  * @param $key
  *   The key specified in the $sources array in
  *   hook_feeds_parser_sources_alter().
@@ -167,31 +163,13 @@ function hook_feeds_parser_sources_alter
  * @see hook_feeds_parser_sources_alter().
  * @see locale_feeds_get_source().
  */
-function my_source_get_source(FeedsImportBatch $batch, $key) {
-  $item = $batch->currentItem();
+function my_source_get_source($source, FeedsParserResult $result, $key) {
+  $item = $result->currentItem();
   return my_source_parse_images($item['description']);
 }
 
 /**
- * Alter mapping targets for users. Use this hook to add additional target
- * options to the mapping form of User processors.
- *
- * For an example implementation, see mappers/profile.inc
- *
- * @param: &$targets
- *  Array containing the targets to be offered to the user. Add to this array
- *  to expose additional options. Remove from this array to suppress options.
- */
-function hook_feeds_user_processor_targets_alter(&$targets) {
-  $targets['my_user_field'] = array(
-    'name' => t('My custom user field'),
-    'description' => t('Description of what my custom user field does.'),
-    'callback' => 'my_callback',
-  );
-}
-
-/**
- * Alter mapping targets for nodes. Use this hook to add additional target
+ * Alter mapping targets for entities. Use this hook to add additional target
  * options to the mapping form of Node processors.
  *
  * If the key in $targets[] does not correspond to the actual key on the node
@@ -203,60 +181,45 @@ function hook_feeds_user_processor_targe
  *   Array containing the targets to be offered to the user. Add to this array
  *   to expose additional options. Remove from this array to suppress options.
  *   Remove with caution.
- * @param $content_type
- *   The content type of the target node.
- */
-function hook_feeds_node_processor_targets_alter(&$targets, $content_type) {
-  $targets['my_node_field'] = array(
-    'name' => t('My custom node field'),
-    'description' => t('Description of what my custom node field does.'),
-    'callback' => 'my_callback',
-  );
-  $targets['my_node_field2'] = array(
-    'name' => t('My Second custom node field'),
-    'description' => t('Description of what my second custom node field does.'),
-    'callback' => 'my_callback2',
-    'real_target' => 'my_node_field_two', // Specify real target field on node.
-  );
-}
-
-/**
- * Alter mapping targets for taxonomy terms. Use this hook to add additional
- * target options to the mapping form of Taxonomy term processor.
- *
- * For an example implementation, look at geotaxnomy module.
- * http://drupal.org/project/geotaxonomy
- *
- * @param &$targets
- *   Array containing the targets to be offered to the user. Add to this array
- *   to expose additional options. Remove from this array to suppress options.
- *   Remove with caution.
- * @param $vid
- *   The vocabulary id
- */
-function hook_feeds_term_processor_targets_alter(&$targets, $vid) {
-  if (variable_get('mymodule_vocabulary_'. $vid, 0)) {
-    $targets['lat'] = array(
-      'name' => t('Latitude'),
-      'description' => t('Latitude of the term.'),
+ * @param $entity_type
+ *   The entity type of the target, for instance a 'node' entity.
+ * @param $bundle_name
+ *   The bundle name for which to alter targets.
+ */
+function hook_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+  if ($entity_type == 'node') {
+    $targets['my_node_field'] = array(
+      'name' => t('My custom node field'),
+      'description' => t('Description of what my custom node field does.'),
+      'callback' => 'my_module_set_target',
     );
-    $targets['lon'] = array(
-      'name' => t('Longitude'),
-      'description' => t('Longitude of the term.'),
+    $targets['my_node_field2'] = array(
+      'name' => t('My Second custom node field'),
+      'description' => t('Description of what my second custom node field does.'),
+      'callback' => 'my_module_set_target2',
+      'real_target' => 'my_node_field_two', // Specify real target field on node.
     );
   }
 }
 
 /**
- * Alter mapping targets for Data table entries. Use this hook to add additional
- * target options to the mapping form of Data processor.
+ * Example callback specified in hook_feeds_processor_targets_alter().
+ *
+ * @param $source
+ *   Field mapper source settings.
+ * @param $entity
+ *   An entity object, for instance a node object.
+ * @param $target
+ *   A string identifying the target on the node.
+ * @param $value
+ *   The value to populate the target with.
+ *
  */
-function hook_feeds_data_processor_targets_alter(&$fields, $data_table) {
-  if ($data_table == mymodule_base_table()) {
-    $fields['mytable:category'] = array(
-      'name' => t('Category'),
-      'description' => t('One or more category terms.'),
-    );
+function my_module_set_target($source, $entity, $target, $value) {
+  $entity->$target['und'][0]['value'] = $value;
+  if (isset($source->importer->processor->config['input_format'])) {
+    $entity->$target['und'][0]['format'] =
+      $source->importer->processor->config['input_format'];
   }
 }
 
Index: feeds.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.install,v
retrieving revision 1.13.2.1
diff -u -p -r1.13.2.1 feeds.install
--- feeds.install	25 Sep 2010 16:07:50 -0000	1.13.2.1
+++ feeds.install	3 Feb 2011 13:03:53 -0000
@@ -68,15 +68,29 @@ function feeds_schema() {
       'source' => array(
         'type' => 'text',
         'not null' => TRUE,
-        'description' => t('Main source resource identifier. E. g. a path or a URL.'),
+        'description' => 'Main source resource identifier. E. g. a path or a URL.',
       ),
-      'batch' => array(
+      'state' => array(
         'type' => 'text',
         'size' => 'big',
         'not null' => FALSE,
-        'description' => t('Cache for batching.'),
+        'description' => 'State of import or clearing batches.',
         'serialize' => TRUE,
       ),
+      'fetcher_result' => array(
+        'type' => 'text',
+        'size' => 'big',
+        'not null' => FALSE,
+        'description' => 'Cache for fetcher result.',
+        'serialize' => TRUE,
+      ),
+      'imported' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'unsigned' => TRUE,
+        'description' => 'Timestamp when this source was imported last.',
+      ),
     ),
     'primary key' => array('id', 'feed_nid'),
     'indexes' => array(
@@ -85,89 +99,68 @@ function feeds_schema() {
       'id_source' => array('id', array('source', 128)),
     ),
   );
-  $schema['feeds_node_item'] = array(
-    'description' => t('Stores additional information about feed item nodes. Used by FeedsNodeProcessor.'),
+  $schema['feeds_item'] = array(
+    'description' => 'Tracks items such as nodes, terms, users.',
     'fields' => array(
-      'nid' => array(
+      'entity_type' => array(
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'The entity type.',
+      ),
+      'entity_id' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
-        'description' => t("Primary Key: The feed item node's nid."),
+        'description' => 'The imported entity\'s serial id.',
       ),
       'id' => array(
         'type' => 'varchar',
         'length' => 128,
         'not null' => TRUE,
         'default' => '',
-        'description' => 'The id of the fields object that is the producer of this item.',
+        'description' => 'The id of the importer that created this item.',
       ),
       'feed_nid' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
-        'description' => t("Node id of the owner feed, if available."),
+        'description' => 'Node id of the source, if available.',
       ),
       'imported' => array(
         'type' => 'int',
         'not null' => TRUE,
         'default' => 0,
-        'description' => t('Import date of the feed item, as a Unix timestamp.'),
+        'description' => 'Import date of the feed item, as a Unix timestamp.',
       ),
       'url' => array(
         'type' => 'text',
         'not null' => TRUE,
-        'description' => t('Link to the feed item.'),
+        'description' => 'Link to the feed item.',
       ),
       'guid' => array(
         'type' => 'text',
         'not null' => TRUE,
-        'description' => t('Unique identifier for the feed item.'),
+        'description' => 'Unique identifier for the feed item.'
       ),
       'hash' => array(
         'type' => 'varchar',
         'length' => 32, // The length of an MD5 hash.
         'not null' => TRUE,
         'default' => '',
-        'description' => t('The hash of the item.'),
+        'description' => 'The hash of the source item.',
       ),
     ),
-    'primary key' => array('nid'),
+    'primary key' => array('entity_type', 'entity_id'),
     'indexes' => array(
       'id' => array('id'),
       'feed_nid' => array('feed_nid'),
+      'lookup_url' => array('entity_type', 'id', 'feed_nid', array('url', 255)),
+      'lookup_guid' => array('entity_type', 'id', 'feed_nid', array('guid', 255)),
+      'global_lookup_url' => array('entity_type', array('url', 255)),
+      'global_lookup_guid' => array('entity_type', array('guid', 255)),
       'imported' => array('imported'),
-      'url' => array(array('url', 255)),
-      'guid' => array(array('guid', 255)),
-    ),
-  );
-  $schema['feeds_term_item'] = array(
-    'description' => 'Tracks imported terms.',
-    'fields' => array(
-      'tid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Imported term id.',
-      ),
-      'id' => array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The id of the fields object that is the creator of this item.',
-      ),
-      'feed_nid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => t("Node id of the owner feed, if available."),
-      ),
-    ),
-    'primary key' => array('tid'),
-    'indexes' => array(
-      'id_feed_nid' => array('id', 'feed_nid'),
-      'feed_nid' => array('feed_nid'),
     ),
   );
   $schema['feeds_push_subscriptions'] = array(
@@ -197,12 +190,12 @@ function feeds_schema() {
       'hub' => array(
         'type' => 'text',
         'not null' => TRUE,
-        'description' => t('The URL of the hub endpoint of this subscription.'),
+        'description' => 'The URL of the hub endpoint of this subscription.',
       ),
       'topic' => array(
         'type' => 'text',
         'not null' => TRUE,
-        'description' => t('The topic URL (feed URL) of this subscription.'),
+        'description' => 'The topic URL (feed URL) of this subscription.',
       ),
       'secret' => array(
         'type' => 'varchar',
@@ -230,340 +223,91 @@ function feeds_schema() {
       'timestamp' => array('timestamp'),
     ),
   );
-  return $schema;
-}
-
-/**
- * Implementation of hook_install().
- */
-function feeds_install() {
-  // Create tables.
-  drupal_install_schema('feeds');
-}
-
-/**
- * Implementation of hook_uninstall().
- */
-function feeds_uninstall() {
-  // Remove tables.
-  drupal_uninstall_schema('feeds');
-}
-
-/**
- * Remove class field on feeds_config.
- */
-function feeds_update_6001() {
-  $ret = array();
-  db_drop_field($ret, 'feeds_config', 'class');
-  return $ret;
-}
-
-/**
- * Rename table.
- */
-function feeds_update_6002() {
-  $ret = array();
-  db_rename_table($ret, 'feeds_config', 'feeds_importer');
-  return $ret;
-}
-
-/**
- * Add primary keys to feeds_importer and feeds_source.
- */
-function feeds_update_6003() {
-  $ret = array();
-  db_drop_index($ret, 'feeds_importer', 'id');
-  db_add_primary_key($ret, 'feeds_importer', array('id'));
-  db_add_primary_key($ret, 'feeds_source', array('id', 'feed_nid'));
-  return $ret;
-}
-
-/**
- * Add source field to feeds_source, make fields part of PKs not null.
- */
-function feeds_update_6004() {
-  $ret = array();
-
-  $spec = array(
-    'type' => 'text',
-    'not null' => TRUE,
-    'description' => t('Main source resource identifier. E. g. a path or a URL.'),
-  );
-  db_add_field($ret, 'feeds_source', 'source', $spec);
-  db_add_index($ret, 'feeds_source', 'id_source', array('id', array('source', 255)));
-
-  // Make feed_nid not null, default 0. It is part of the primary key.
-  $spec = array(
-    'type' => 'int',
-    'not null' => TRUE,
-    'default' => 0,
-    'unsigned' => TRUE,
-    'description' => 'Node nid if this particular source is attached to a feed node.',
-  );
-  db_change_field($ret, 'feeds_schedule', 'feed_nid', 'feed_nid', $spec);
-
-
-  // Same thing for feeds_source table.
-  $spec = array(
-    'type' => 'int',
-    'not null' => TRUE,
-    'default' => 0,
-    'unsigned' => TRUE,
-    'description' => 'Node nid if this particular source is attached to a feed node.',
-  );
-  db_change_field($ret, 'feeds_source', 'feed_nid', 'feed_nid', $spec);
-
-  return $ret;
-}
-
-/**
- * Add callback column to feeds_schedule.
- */
-function feeds_update_6005() {
-  $ret = array();
-
-  // Add a callback column and an index.
-  $spec = array(
-    'type' => 'varchar',
-    'length' => 128,
-    'not null' => TRUE,
-    'default' => '',
-    'description' => 'Callback to be invoked.',
-  );
-  db_add_field($ret, 'feeds_schedule', 'callback', $spec);
-
-  db_add_index($ret, 'feeds_schedule', 'id_callback', array('id', 'callback'));
-
-  return $ret;
-}
-
-/**
- * Remove primary key from feeds_schedule and replace it by an index.
- */
-function feeds_update_6006() {
-  $ret = array();
-
-  db_drop_primary_key($ret, 'feeds_schedule');
-  db_add_index($ret, 'feeds_schedule', 'feed_nid', array('feed_nid'));
-
-  return $ret;
-}
-
-/**
- * Add hash column to feeds_node_item.
- */
-function feeds_update_6007() {
-  $ret = array();
-
-  $spec = array(
-    'type' => 'varchar',
-    'length' => 32,
-    'not null' => TRUE,
-    'default' => '',
-    'description' => t('The hash of the item.'),
-  );
-  db_add_field($ret, 'feeds_node_item', 'hash', $spec);
-
-  return $ret;
-}
-
-/**
- * Add batch field to feeds_source table, adjust feeds_schedule table.
- */
-function feeds_update_6008() {
-  $ret = array();
-
-  $spec = array(
-    'type' => 'text',
-    'size' => 'big',
-    'not null' => FALSE,
-    'description' => t('Cache for batching.'),
-    'serialize' => TRUE,
-  );
-  db_add_field($ret, 'feeds_source', 'batch', $spec);
-
-  // Make scheduled flag a timestamp.
-  $spec = array(
-    'type' => 'int',
-    'size' => 'normal',
-    'unsigned' => TRUE,
-    'default' => 0,
-    'not null' => TRUE,
-    'description' => 'Timestamp when a job was scheduled. 0 if a job is currently not scheduled.',
-  );
-  db_change_field($ret, 'feeds_schedule', 'scheduled', 'scheduled', $spec);
-
-  // Rename last_scheduled_time to last_executed_time, fix unsigned property.
-  $spec = array(
-    'type' => 'int',
-    'size' => 'normal',
-    'unsigned' => TRUE,
-    'default' => 0,
-    'not null' => TRUE,
-    'description' => 'Timestamp when a job was last executed.',
-  );
-  db_change_field($ret, 'feeds_schedule', 'last_scheduled_time', 'last_executed_time', $spec);
-
-  return $ret;
-}
-
-/**
- * Add feeds_push_subscriptions tables.
- */
-function feeds_update_6009() {
-  $ret = array();
-  $table = array(
-    'description' => 'PubSubHubbub subscriptions.',
+  $schema['feeds_log'] = array(
+    'description' => 'Table that contains logs of feeds events.',
     'fields' => array(
-      'domain' => array(
+      'flid' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'description' => 'Primary Key: Unique feeds event ID.',
+      ),
+      'id' => array(
         'type' => 'varchar',
         'length' => 128,
         'not null' => TRUE,
         'default' => '',
-        'description' => 'Domain of the subscriber. Corresponds to an importer id.',
+        'description' => 'The id of the importer that logged the event.',
       ),
-      'subscriber_id' => array(
+      'feed_nid' => array(
         'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
         'unsigned' => TRUE,
-        'description' => 'ID of the subscriber. Corresponds to a feed nid.',
-      ),
-      'timestamp' => array(
-        'type' => 'int',
-        'unsigned' => FALSE,
-        'default' => 0,
         'not null' => TRUE,
-        'description' => 'Created timestamp.',
-      ),
-      'hub' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'description' => t('The URL of the hub endpoint of this subscription.'),
+        'description' => 'Node id of the source, if available.',
       ),
-      'topic' => array(
-        'type' => 'text',
+      'log_time' => array(
+        'type' => 'int',
         'not null' => TRUE,
-        'description' => t('The topic URL (feed URL) of this subscription.'),
+        'default' => 0,
+        'description' => 'Unix timestamp of when event occurred.',
       ),
-      'secret' => array(
-        'type' => 'varchar',
-        'length' => 128,
+      'request_time' => array(
+        'type' => 'int',
         'not null' => TRUE,
-        'default' => '',
-        'description' => 'Shared secret for message authentication.',
+        'default' => 0,
+        'description' => 'Unix timestamp of the request when the event occurred.',
       ),
-      'status' => array(
+      'type' => array(
         'type' => 'varchar',
         'length' => 64,
         'not null' => TRUE,
         'default' => '',
-        'description' => 'Status of subscription.',
+        'description' => 'Type of log message, for example "feeds_import"."',
       ),
-      'post_fields' => array(
+      'message' => array(
         'type' => 'text',
-        'not null' => FALSE,
-        'description' => 'Fields posted.',
-        'serialize' => TRUE,
-      ),
-    ),
-    'primary key' => array('domain', 'subscriber_id'),
-    'indexes' => array(
-      'timestamp' => array('timestamp'),
-    ),
-  );
-  db_create_table($ret, 'feeds_push_subscriptions', $table);
-  return $ret;
-}
-
-/**
- * Enable all Feeds News, Feeds Import and Feeds fast news features.
- */
-function feeds_update_6010() {
-  drupal_install_modules(array('feeds_news', 'feeds_import'));
-  if (module_exists('data')) {
-    drupal_install_modules(array('feeds_fast_news'));
-    drupal_set_message(t('Installed Feeds News, Feeds Fast News and Feeds Import as replacement for Feeds Defaults module. If you were not using Feeds Defaults then you can safely disable Feeds News and Feeds Import.'));
-  }
-  else {
-    drupal_set_message(t('Installed Feeds News and Feeds Import as replacement for Feeds Defaults module. If you were not using Feeds Defaults then you can safely disable Feeds News and Feeds Import.'));
-  }
-  if (module_exists('features')) {
-    drupal_set_message(t('<strong>Review enabled state of importer configurations on admin/build/feeds and features on admin/build/features.</strong>'));
-  }
-  else {
-    drupal_set_message(t('<strong>Review enabled state of importer configurations on admin/build/feeds and Feeds modules on admin/build/modules.</strong>'));
-  }
-  return array();
-}
-
-/**
- * Add imported flag for terms.
- */
-function feeds_update_6011() {
-  $ret = array();
-  $schema = array(
-    'description' => 'Tracks imported terms.',
-    'fields' => array(
-      'tid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
         'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Imported term id.',
+        'size' => 'big',
+        'description' => 'Text of log message to be passed into the t() function.',
       ),
-      'id' => array(
-        'type' => 'varchar',
-        'length' => 128,
+      'variables' => array(
+        'type' => 'blob',
         'not null' => TRUE,
-        'default' => '',
-        'description' => 'The id of the fields object that is the creator of this item.',
+        'size' => 'big',
+        'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
       ),
-      'feed_nid' => array(
+      'severity' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
-        'description' => t("Node id of the owner feed, if available."),
+        'default' => 0,
+        'size' => 'tiny',
+        'description' => 'The severity level of the event; ranges from 0 (Emergency) to 7 (Debug)',
       ),
     ),
-    'primary key' => array('tid'),
+    'primary key' => array('flid'),
     'indexes' => array(
+      'id' => array('id'),
       'id_feed_nid' => array('id', 'feed_nid'),
-      'feed_nid' => array('feed_nid'),
+      'request_time' => array('request_time'),
+      'log_time' => array('log_time'),
+      'type' => array('type'),
     ),
   );
-  db_create_table($ret, 'feeds_term_item', $schema);
-  return $ret;
+  return $schema;
 }
 
 /**
- * Drop schedule table install Job Scheduler module.
+ * Implementation of hook_install().
  */
-function feeds_update_6012() {
-  $ret = array();
-  // Only attempt installation if module is present, otherwise we would leave
-  // the system table in a limbo.
-  $modules = module_rebuild_cache();
-  if (isset($modules['job_scheduler'])) {
-    if (!$modules['job_scheduler']->status) {
-      drupal_install_modules(array('job_scheduler'));
-      drupal_set_message(t('Installed Job Scheduler module.'));
-    }
-  }
-  else {
-    drupal_set_message(t('NOTE: Please install new dependency of Feeds: !job_scheduler module.', array('!job_scheduler' => l(t('Job Scheduler'), 'http://drupal.org/project/job_scheduler'))), 'warning');
-  }
-  db_drop_table($ret, 'feeds_schedule');
-  return $ret;
+function feeds_install() {
+  // Create tables.
+  drupal_install_schema('feeds');
 }
 
 /**
- * Reschedule all tasks.
+ * Implementation of hook_uninstall().
  */
-function feeds_update_6013() {
-  // This was originally part of 6012 upgrade, but failed due to usage of
-  // API functions residing in feeds.module. Make sure it runs again for all
-  // users of Feeds.
-  variable_set('feeds_reschedule', TRUE);
-  return array();
+function feeds_uninstall() {
+  // Remove tables.
+  drupal_uninstall_schema('feeds');
 }
Index: feeds.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.module,v
retrieving revision 1.55.2.3
diff -u -p -r1.55.2.3 feeds.module
--- feeds.module	28 Oct 2010 19:58:22 -0000	1.55.2.3
+++ feeds.module	3 Feb 2011 13:03:54 -0000
@@ -15,8 +15,8 @@ define('FEEDS_EXPIRE_NEVER', -1);
 // An object that is not persistent. Compare EXPORT_IN_DATABASE, EXPORT_IN_CODE.
 define('FEEDS_EXPORT_NONE', 0x0);
 // Status of batched operations.
-define('FEEDS_BATCH_COMPLETE', 1);
-define('FEEDS_BATCH_ACTIVE', 0);
+define('FEEDS_BATCH_COMPLETE', 1.0);
+define('FEEDS_BATCH_ACTIVE', 0.0);
 
 /**
  * @defgroup hooks Hook and callback implementations
@@ -36,8 +36,27 @@ function feeds_cron() {
       }
     }
     feeds_reschedule(FALSE);
-    return;
   }
+  db_query("DELETE FROM {feeds_log} WHERE request_time < %d", FEEDS_REQUEST_TIME - 604800);
+}
+
+/**
+ * Implementation of hook_cron_job_scheduler_info().
+ *
+ * Compare queue names with key names in feeds_cron_queue_info().
+ */
+function feeds_cron_job_scheduler_info() {
+  $info = array();
+  $info['feeds_source_import'] = array(
+    'queue name' => 'feeds_source_import',
+  );
+  $info['feeds_source_clear'] = array(
+    'queue name' => 'feeds_source_clear',
+  );
+  $info['feeds_importer_expire'] = array(
+    'queue name' => 'feeds_importer_expire',
+  );
+  return $info;
 }
 
 /**
@@ -49,11 +68,15 @@ function feeds_cron_queue_info() {
   $queues = array();
   $queues['feeds_source_import'] = array(
     'worker callback' => 'feeds_source_import',
-    'time' => variable_get('feeds_worker_time', 15),
+    'time' => 15,
+  );
+  $queues['feeds_source_clear'] = array(
+    'worker callback' => 'feeds_source_clear',
+    'time' => 15,
   );
   $queues['feeds_importer_expire'] = array(
     'worker callback' => 'feeds_importer_expire',
-    'time' => variable_get('feeds_worker_time', 15),
+    'time' => 15,
   );
   return $queues;
 }
@@ -68,9 +91,24 @@ function feeds_source_import($job) {
   }
   catch (FeedsNotExistingException $e) {}
   catch (Exception $e) {
-    watchdog('feeds_source_import()', $e->getMessage(), array(), WATCHDOG_ERROR);
+    $source->log('import', $e->getMessage(), array(), WATCHDOG_ERROR);
   }
-  $source->schedule();
+  $source->scheduleImport();
+}
+
+/**
+ * Scheduler callback for deleting all items from a source.
+ */
+function feeds_source_clear($job) {
+  $source = feeds_source($job['type'], $job['id']);
+  try {
+    $source->existing()->clear();
+  }
+  catch (FeedsNotExistingException $e) {}
+  catch (Exception $e) {
+    $source->log('clear', $e->getMessage(), array(), WATCHDOG_ERROR);
+  }
+  $source->scheduleClear();
 }
 
 /**
@@ -83,17 +121,40 @@ function feeds_importer_expire($job) {
   }
   catch (FeedsNotExistingException $e) {}
   catch (Exception $e) {
-    watchdog('feeds_importer_expire()', $e->getMessage(), array(), WATCHDOG_ERROR);
+    $importer->log('expire', $e->getMessage(), array(), WATCHDOG_ERROR);
   }
-  $importer->schedule();
+  $importer->scheduleExpire();
 }
 
 /**
- * Reschedule one or all importers.
+ * Batch API worker callback. Used by FeedsSource::startBatchAPIJob().
  *
- * Note: variable_set('feeds_reschedule', TRUE) is used in update hook
- * feeds_update_6013() and as such must be maintained as part of the upgrade
- * path from pre 6.x 1.0 beta 6 versions of Feeds.
+ * @see FeedsSource::startBatchAPIJob().
+ *
+ * @todo Harmonize Job Scheduler API callbacks with Batch API callbacks?
+ *
+ * @param $method
+ *   Method to execute on importer; one of 'import' or 'clear'.
+ * @param $importer_id
+ *   Identifier of a FeedsImporter object.
+ * @param $feed_nid
+ *   If importer is attached to content type, feed node id identifying the
+ *   source to be imported.
+ * @param $context
+ *   Batch context.
+ */
+function feeds_batch($method, $importer_id, $feed_nid = 0, &$context) {
+  $context['finished'] = FEEDS_BATCH_COMPLETE;
+  try {
+    $context['finished'] = feeds_source($importer_id, $feed_nid)->$method();
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+  }
+}
+
+/**
+ * Reschedule one or all importers.
  *
  * @param $importer_id
  *   If TRUE, all importers will be rescheduled, if FALSE, no importers will
@@ -126,8 +187,8 @@ function feeds_reschedule($importer_id =
 function feeds_perm() {
   $perms = array('administer feeds');
   foreach (feeds_importer_load_all() as $importer) {
-    $perms[] = 'import '. $importer->id .' feeds';
-    $perms[] = 'clear '. $importer->id .' feeds';
+    $perms[] = "import $importer->id feeds";
+    $perms[] = "clear $importer->id feeds";
   }
   return $perms;
 }
@@ -140,7 +201,10 @@ function feeds_perm() {
 function feeds_forms() {
   $forms = array();
   $forms['FeedsImporter_feeds_form']['callback'] = 'feeds_form';
-  $plugins = feeds_get_plugins();
+  if (!class_exists('FeedsPlugin')) {
+    feeds_include('FeedsPlugin', 'plugins');
+  }
+  $plugins = FeedsPlugin::all();
   foreach ($plugins as $plugin) {
     $forms[$plugin['handler']['class'] .'_feeds_form']['callback'] = 'feeds_form';
   }
@@ -151,65 +215,69 @@ function feeds_forms() {
  * Implementation of hook_menu().
  */
 function feeds_menu() {
-  // Register a callback for all feed configurations that are not attached to a content type.
   $items = array();
+  $items['import'] = array(
+    'title' => 'Import',
+    'page callback' => 'feeds_page',
+    'access callback' => 'feeds_page_access',
+    'file' => 'feeds.pages.inc',
+  );
+  $items['import/%'] = array(
+    'title callback' => 'feeds_importer_title',
+    'title arguments' => array(1),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feeds_import_form', 1),
+    'access callback' => 'feeds_access',
+    'access arguments' => array('import', 1),
+    'file' => 'feeds.pages.inc',
+  );
+  $items['import/%/import'] = array(
+    'title' => 'Import',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['import/%/delete-items'] = array(
+    'title' => 'Delete items',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feeds_delete_tab_form', 1),
+    'access callback' => 'feeds_access',
+    'access arguments' => array('clear', 1),
+    'file' => 'feeds.pages.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['import/%/template'] = array(
+    'page callback' => 'feeds_importer_template',
+    'page arguments' => array(1),
+    'access callback' => 'feeds_access',
+    'access arguments' => array('import', 1),
+    'file' => 'feeds.pages.inc',
+    'type' => MENU_CALLBACK,
+  );
+  $items['node/%node/import'] = array(
+    'title' => 'Import',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feeds_import_tab_form', 1),
+    'access callback' => 'feeds_access',
+    'access arguments' => array('import', 1),
+    'file' => 'feeds.pages.inc',
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 10,
+  );
+  $items['node/%node/delete-items'] = array(
+    'title' => 'Delete items',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feeds_delete_tab_form', NULL, 1),
+    'access callback' => 'feeds_access',
+    'access arguments' => array('clear', 1),
+    'file' => 'feeds.pages.inc',
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 11,
+  );
+  // @todo Eliminate this step and thus eliminate clearing menu cache when
+  // manipulating importers.
   foreach (feeds_importer_load_all() as $importer) {
-    if (empty($importer->config['content_type'])) {
-      $items['import/'. $importer->id] = array(
-        'title' => $importer->config['name'],
-        'page callback' => 'drupal_get_form',
-        'page arguments' => array('feeds_import_form', 1),
-        'access callback' => 'feeds_access',
-        'access arguments' => array('import', $importer->id),
-        'file' => 'feeds.pages.inc',
-      );
-      $items['import/'. $importer->id .'/import'] = array(
-        'title' => 'Import',
-        'type' => MENU_DEFAULT_LOCAL_TASK,
-        'weight' => -10,
-      );
-      $items['import/'. $importer->id .'/delete-items'] = array(
-        'title' => 'Delete items',
-        'page callback' => 'drupal_get_form',
-        'page arguments' => array('feeds_delete_tab_form', 1),
-        'access callback' => 'feeds_access',
-        'access arguments' => array('clear', $importer->id),
-        'file' => 'feeds.pages.inc',
-        'type' => MENU_LOCAL_TASK,
-      );
-    }
-    else {
-      $items['node/%node/import'] = array(
-        'title' => 'Import',
-        'page callback' => 'drupal_get_form',
-        'page arguments' => array('feeds_import_tab_form', 1),
-        'access callback' => 'feeds_access',
-        'access arguments' => array('import', 1),
-        'file' => 'feeds.pages.inc',
-        'type' => MENU_LOCAL_TASK,
-        'weight' => 10,
-      );
-      $items['node/%node/delete-items'] = array(
-        'title' => 'Delete items',
-        'page callback' => 'drupal_get_form',
-        'page arguments' => array('feeds_delete_tab_form', NULL, 1),
-        'access callback' => 'feeds_access',
-        'access arguments' => array('clear', 1),
-        'file' => 'feeds.pages.inc',
-        'type' => MENU_LOCAL_TASK,
-        'weight' => 11,
-      );
-    }
     $items += $importer->fetcher->menuItem();
   }
-  if (count($items)) {
-    $items['import'] = array(
-      'title' => 'Import',
-      'page callback' => 'feeds_page',
-      'access callback' => 'feeds_page_access',
-      'file' => 'feeds.pages.inc',
-    );
-  }
   return $items;
 }
 
@@ -221,12 +289,30 @@ function feeds_importer_load($id) {
 }
 
 /**
+ * Title callback.
+ */
+function feeds_importer_title($id) {
+  $importer = feeds_importer($id);
+  return $importer->config['name'];
+}
+
+/**
  * Implementation of hook_theme().
  */
 function feeds_theme() {
   return array(
     'feeds_upload' => array(
       'file' => 'feeds.pages.inc',
+      'render element' => 'element',
+    ),
+    'feeds_source_status' => array(
+      'file' => 'feeds.pages.inc',
+      'variables' => array(
+        'progress_importing' => NULL,
+        'progress_clearing' => NULL,
+        'imported' => NULL,
+        'count' => NULL,
+      ),
     ),
   );
 }
@@ -272,6 +358,15 @@ function feeds_page_access() {
 }
 
 /**
+ * Implementation of hook_exit().
+ */
+function feeds_exit() {
+  if (ctools_static('feeds_log_error', FALSE)) {
+    watchdog('feeds', 'Feeds reported errors, visit the Feeds log for details.', array(), WATCHDOG_ERROR, 'admin/reports/dblog/feeds');
+  }
+}
+
+/**
  * Implementation of hook_views_api().
  */
 function feeds_views_api() {
@@ -313,116 +408,137 @@ function feeds_feeds_plugins() {
 /**
  * Implementation of hook_nodeapi().
  *
- * @todo For Drupal 7, revisit static cache based shuttling of values between
- *   'validate' and 'update'/'insert'.
  */
 function feeds_nodeapi(&$node, $op, $form) {
 
-  // $node looses any changes after 'validate' stage (see node_form_validate()).
-  // Keep a copy of title and feeds array between 'validate' and subsequent
-  // stages. This allows for automatically populating the title of the node form
-  // and modifying the $form['feeds'] array on node validation just like on the
-  // standalone form.
-  static $last_title;
-  static $node_feeds;
+  switch ($op) {
+    case 'validate':
+      $form_state = array();
+      feeds_node_validate($node, $form, $form_state);
+      break;
+    case 'presave':
+      feeds_node_presave($node);
+      break;
+    case 'insert':
+      feeds_node_insert($node);
+      break;
+    case 'update':
+      feeds_node_update($node);
+      break;
+    case 'delete':
+      feeds_node_delete($node);
+      break;
+  }
+}
+
+/**
+ * Callback for nodeapi op validate.
+ */
+function feeds_node_validate($node, $form, &$form_state) {
+  if (!$importer_id = feeds_get_importer_id($node->type)) {
+    return;
+  }
+  // Keep a copy of the title for subsequent node creation stages.
+  // @todo: revisit whether $node still looses all of its properties
+  // between validate and insert stage.
+  $last_title = &ctools_static('feeds_node_last_title');
+  $last_feeds = &ctools_static('feeds_node_last_feeds');
+
+  // On validation stage we are working with a FeedsSource object that is
+  // not tied to a nid - when creating a new node there is no
+  // $node->nid at this stage.
+  $source = feeds_source($importer_id);
+
+  // Node module magically moved $form['feeds'] to $node->feeds :P.
+  // configFormValidate may modify $last_feed, smuggle it to update/insert stage
+  // through a static variable.
+  $last_feeds = $node->feeds;
+  $source->configFormValidate($last_feeds);
+
+  // If node title is empty, try to retrieve title from feed.
+  if (trim($node->title) == '') {
+    try {
+      $source->addConfig($last_feeds);
+      if (!$last_title = $source->preview()->title) {
+        throw new Exception();
+      }
+    }
+    catch (Exception $e) {
+      drupal_set_message($e->getMessage(), 'error');
+      form_set_error('title', t('Could not retrieve title from feed.'), 'error');
+    }
+  }
+}
+
+/**
+ * Callback for nodeapi op validate.
+ */
+function feeds_node_presave($node) {
+  // Populate $node->title and $node->feed from result of validation phase.
+  $last_title = &ctools_static('feeds_node_last_title');
+  $last_feeds = &ctools_static('feeds_node_last_feeds');
+  if (empty($node->title) && !empty($last_title)) {
+    $node->title = $last_title;
+  }
+  if (!empty($last_feeds)) {
+    $node->feeds = $last_feeds;
+  }
+  $last_title = NULL;
+  $last_feeds = NULL;
+}
 
-  // Break out node processor related nodeapi functionality.
-  _feeds_nodeapi_node_processor($node, $op);
+/**
+ * Callback for nodeapi op validate.
+ */
+function feeds_node_insert($node) {
+  // Node produced by source.
+  feeds_item_info_insert($node, $node->nid);
 
+  // Source attached to node.
+  feeds_node_update($node);
   if ($importer_id = feeds_get_importer_id($node->type)) {
-    switch ($op) {
-      case 'validate':
-        // On validation stage we are working with a FeedsSource object that is
-        // not tied to a nid - when creating a new node there is no
-        // $node->nid at this stage.
-        $source = feeds_source($importer_id);
-
-        // Node module magically moved $form['feeds'] to $node->feeds :P
-        $node_feeds = $node->feeds;
-        $source->configFormValidate($node_feeds);
-
-        // If node title is empty, try to retrieve title from feed.
-        if (trim($node->title) == '') {
-          try {
-            $source->addConfig($node_feeds);
-            if (!$last_title = $source->preview()->getTitle()) {
-              throw new Exception();
-            }
-          }
-          catch (Exception $e) {
-            drupal_set_message($e->getMessage(), 'error');
-            form_set_error('title', t('Could not retrieve title from feed.'), 'error');
-          }
-        }
-        break;
-      case 'presave':
-        if (!empty($last_title)) {
-          $node->title = $last_title;
-        }
-        $last_title = NULL;
-        break;
-      case 'insert':
-      case 'update':
-        // A node may not have been validated, make sure $node_feeds is present.
-        if (empty($node_feeds)) {
-          $node_feeds = $node->feeds;
-        }
-        // Add configuration to feed source and save.
-        $source = feeds_source($importer_id, $node->nid);
-        $source->addConfig($node_feeds);
-        $source->save();
-
-        // Refresh feed if import on create is selected and suppress_import is
-        // not set.
-        if ($op == 'insert' && feeds_importer($importer_id)->config['import_on_create'] && !isset($node_feeds['suppress_import'])) {
-          feeds_batch_set(t('Importing'), 'import', $importer_id, $node->nid);
-        }
-        // Add source to schedule, make sure importer is scheduled, too.
-        if ($op == 'insert') {
-          $source->schedule();
-          $source->importer->schedule();
-        }
-        $node_feeds = NULL;
-        break;
-      case 'delete':
-        $source = feeds_source($importer_id, $node->nid);
-        if ($source->importer->processor->config['delete_with_source']) {
-          feeds_batch_set(t('Deleting'), 'clear', $importer_id, $node->nid);
-        }
-        // Remove attached source.
-        $source->delete();
-        break;
+    $source = feeds_source($importer_id, $node->nid);
+    // Start import if requested.
+    if (feeds_importer($importer_id)->config['import_on_create'] && !isset($node->feeds['suppress_import'])) {
+      $source->startImport();
     }
+    // Schedule source and importer.
+    $source->schedule();
+    feeds_importer($importer_id)->schedule();
   }
 }
 
 /**
- * Handles FeedsNodeProcessor specific nodeapi operations.
+ * Callback for nodeapi op validate.
  */
-function _feeds_nodeapi_node_processor($node, $op) {
-  switch ($op) {
-    case 'load':
-      if ($result = db_fetch_object(db_query("SELECT imported, guid, url, feed_nid FROM {feeds_node_item} WHERE nid = %d", $node->nid))) {
-        $node->feeds_node_item = $result;
-      }
-      break;
-    case 'insert':
-      if (isset($node->feeds_node_item)) {
-        $node->feeds_node_item->nid = $node->nid;
-        drupal_write_record('feeds_node_item', $node->feeds_node_item);
-      }
-      break;
-    case 'update':
-      if (isset($node->feeds_node_item)) {
-        $node->feeds_node_item->nid = $node->nid;
-        drupal_write_record('feeds_node_item', $node->feeds_node_item, 'nid');
-      }
-      break;
-    case 'delete':
-      if (isset($node->feeds_node_item)) {
-        db_query("DELETE FROM {feeds_node_item} WHERE nid = %d", $node->nid);
-      }
-      break;
+function feeds_node_update($node) {
+  // Node produced by source.
+  feeds_item_info_save($node, $node->nid);
+
+  // Source attached to node.
+  if ($importer_id = feeds_get_importer_id($node->type)) {
+    $source = feeds_source($importer_id, $node->nid);
+    $source->addConfig($node->feeds);
+    $source->save();
+  }
+}
+
+/**
+ * Callback for nodeapi op validate.
+ */
+function feeds_node_delete($node) {
+  // Node produced by source.
+  db_query("DELETE FROM {feeds_item} WHERE entity_type = 'node' AND entity_id = %d", $node->nid);
+  // Source attached to node.
+  // Make sure we don't leave any orphans behind: Do not use
+  // feeds_get_importer_id() to determine importer id as the importer may have
+  // been deleted.
+  if ($importer = db_fetch_object(db_query("SELECT id FROM {feeds_source} WHERE feed_nid = %d", $node->nid))) {
+    $source = feeds_source($importer->id, $node->nid);
+    if ($source->importer->processor->config['delete_with_source']) {
+      feeds_batch_set(t('Deleting'), 'clear', $importer->id, $node->nid);
+    }
+    $source->delete();
   }
 }
 
@@ -433,23 +549,40 @@ function feeds_taxonomy($op = NULL, $typ
   if ($type =='term' && $term['tid']) {
     switch ($op) {
       case 'delete':
-        db_query("DELETE FROM {feeds_term_item} WHERE tid = %d", $term['tid']);
+        feeds_taxonomy_term_delete((object) $term);
         break;
       case 'update':
-        if (isset($term['importer_id'])) {
-          db_query("DELETE FROM {feeds_term_item} WHERE tid = %d", $term['tid']);
-        }
+        feeds_taxonomy_term_update((object) $term);
+        break;
       case 'insert':
-        if (isset($term['importer_id'])) {
-          $term['id'] = $term['importer_id'];
-          drupal_write_record('feeds_term_item', $term);
-        }
+        feeds_taxonomy_term_insert((object) $term);
         break;
     }
   }
 }
 
 /**
+ * Callback for hook_taxonomy op insert.
+ */
+function feeds_taxonomy_term_insert($term) {
+  feeds_item_info_insert($term, $term->tid);
+}
+
+/**
+ * Callback for hook_taxonomy op update.
+ */
+function feeds_taxonomy_term_update($term) {
+  feeds_item_info_save($term, $term->tid);
+}
+
+/**
+ * Callback for hook_taxonomy op delete.
+ */
+function feeds_taxonomy_term_delete($term) {
+  db_query("DELETE FROM {feeds_item} WHERE entity_type = 'taxonomy_term' AND entity_id = %d", $term->tid);
+}
+
+/**
  * Implementation of hook_form_alter().
  */
 function feeds_form_alter(&$form, $form_state, $form_id) {
@@ -478,61 +611,6 @@ function feeds_form_alter(&$form, $form_
  */
 
 /**
- * @defgroup batch Batch functions
- */
-
-/**
- * Batch helper.
- *
- * @param $title
- *   Title to show to user when executing batch.
- * @param $method
- *   Method to execute on importer; one of 'import', 'clear' or 'expire'.
- * @param $importer_id
- *   Identifier of a FeedsImporter object.
- * @param $feed_nid
- *   If importer is attached to content type, feed node id identifying the
- *   source to be imported.
- */
-function feeds_batch_set($title, $method, $importer_id, $feed_nid = 0) {
-  $batch = array(
-    'title' => $title,
-    'operations' => array(
-      array('feeds_batch', array($method, $importer_id, $feed_nid)),
-    ),
-    'progress_message' => '',
-  );
-  batch_set($batch);
-}
-
-/**
- * Batch callback.
- *
- * @param $method
- *   Method to execute on importer; one of 'import' or 'clear'.
- * @param $importer_id
- *   Identifier of a FeedsImporter object.
- * @param $feed_nid
- *   If importer is attached to content type, feed node id identifying the
- *   source to be imported.
- * @param $context
- *   Batch context.
- */
-function feeds_batch($method, $importer_id, $feed_nid = 0, &$context) {
-  $context['finished'] = 1;
-  try {
-    $context['finished'] = feeds_source($importer_id, $feed_nid)->$method();
-  }
-  catch (Exception $e) {
-    drupal_set_message($e->getMessage(), 'error');
-  }
-}
-
-/**
- * @}
- */
-
-/**
  * @defgroup utility Utility functions
  * @{
  */
@@ -616,7 +694,7 @@ function feeds_cache_clear($rebuild_menu
   ctools_static_reset('_feeds_importer_digest');
   ctools_include('export');
   ctools_export_load_object_reset('feeds_importer');
-  node_get_types('types', NULL, TRUE);
+  ctools_static_reset('_node_types_build');
   if ($rebuild_menu) {
     menu_rebuild();
   }
@@ -637,9 +715,9 @@ function feeds_export($importer_id, $ind
  * Logs to a file like /mytmp/feeds_my_domain_org.log in temporary directory.
  */
 function feeds_dbg($msg) {
-  if (variable_get('feeds_debug', false)) {
+  if (variable_get('feeds_debug', FALSE)) {
     if (!is_string($msg)) {
-      $msg = var_export($msg, true);
+      $msg = var_export($msg, TRUE);
     }
     $filename = trim(str_replace('/', '_', $_SERVER['HTTP_HOST'] . base_path()), '_');
     $handle = fopen(file_directory_temp() ."/feeds_$filename.log", 'a');
@@ -649,6 +727,62 @@ function feeds_dbg($msg) {
 }
 
 /**
+ * Writes to feeds log.
+ */
+function feeds_log($importer_id, $feed_nid, $type, $message, $variables = array(), $severity = WATCHDOG_NOTICE) {
+  if ($severity < WATCHDOG_NOTICE) {
+    $error = &ctools_static('feeds_log_error', FALSE);
+    $error = TRUE;
+  }
+  $obj = new stdClass();
+  $obj->id = $importer_id;
+  $obj->feed_nid = $feed_nid;
+  $obj->log_time = time();
+  $obj->request_time = FEEDS_REQUEST_TIME;
+  $obj->type = $type;
+  $obj->message = $message;
+  $obj->variables = serialize($variables);
+  $obj->severity = $severity;
+  drupal_write_record('feeds_log', $obj);
+}
+
+/**
+ * Loads an item info object.
+ *
+ * Example usage:
+ *
+ * $info = feeds_item_info_load('node', $node->nid);
+ */
+function feeds_item_info_load($entity_type, $entity_id) {
+  return db_fetch_object(db_query("SELECT * FROM {feeds_item} WHERE entity_type = '%s' AND entity_id = %d", $entity_type, $entity_id));
+}
+
+/**
+ * Inserts an item info object into the feeds_item table.
+ */
+function feeds_item_info_insert($entity, $entity_id) {
+  if (isset($entity->feeds_item)) {
+    $entity->feeds_item->entity_id = $entity_id;
+    drupal_write_record('feeds_item', $entity->feeds_item);
+  }
+}
+
+/**
+ * Inserts or updates an item info object in he feeds_item table.
+ */
+function feeds_item_info_save($entity, $entity_id) {
+  if (isset($entity->feeds_item)) {
+    $entity->feeds_item->entity_id = $entity_id;
+    if (feeds_item_info_load($entity->feeds_item->entity_type, $entity_id)) {
+      drupal_write_record('feeds_item', $entity->feeds_item, array('entity_type', 'entity_id'));
+    }
+    else {
+      feeds_item_info_insert($entity, $entity_id);
+    }
+  }
+}
+
+/**
  * @}
  */
 
@@ -691,67 +825,6 @@ function feeds_source($importer_id, $fee
 }
 
 /**
- * @}
- */
-
-/**
- * @defgroup plugins Plugin functions
- * @{
- *
- * @todo Encapsulate this in a FeedsPluginHandler class, move it to includes/
- * and only load it if we're manipulating plugins.
- */
-
-/**
- * Gets all available plugins. Does not list hidden plugins.
- *
- * @return
- *   An array where the keys are the plugin keys and the values
- *   are the plugin info arrays as defined in hook_feeds_plugins().
- */
-function feeds_get_plugins() {
-  ctools_include('plugins');
-  $plugins = ctools_get_plugins('feeds', 'plugins');
-
-  $result = array();
-  foreach ($plugins as $key => $info) {
-    if (!empty($info['hidden'])) {
-      continue;
-    }
-    $result[$key] = $info;
-  }
-
-  // Sort plugins by name and return.
-  uasort($result, 'feeds_plugin_compare');
-  return $result;
-}
-
-/**
- * Sort callback for feeds_get_plugins().
- */
-function feeds_plugin_compare($a, $b) {
-  return strcasecmp($a['name'], $b['name']);
-}
-
-/**
- * Gets all available plugins of a particular type.
- *
- * @param $type
- *   'fetcher', 'parser' or 'processor'
- */
-function feeds_get_plugins_by_type($type) {
-  $plugins = feeds_get_plugins();
-
-  $result = array();
-  foreach ($plugins as $key => $info) {
-    if ($type == feeds_plugin_type($key)) {
-      $result[$key] = $info;
-    }
-  }
-  return $result;
-}
-
-/**
  * Gets an instance of a class for a given plugin and id.
  *
  * @param $plugin
@@ -765,8 +838,7 @@ function feeds_get_plugins_by_type($type
  * @throws Exception
  *   If plugin can't be instantiated.
  */
-function feeds_plugin_instance($plugin, $id) {
-  feeds_include('FeedsImporter');
+function feeds_plugin($plugin, $id) {
   ctools_include('plugins');
   if ($class = ctools_plugin_load_class('feeds', 'plugins', $plugin, 'handler')) {
     return FeedsConfigurable::instance($class, $id);
@@ -784,60 +856,6 @@ function feeds_plugin_instance($plugin, 
 }
 
 /**
- * Determines whether given plugin is derived from given base plugin.
- *
- * @param $plugin_key
- *   String that identifies a Feeds plugin key.
- * @param $parent_plugin
- *   String that identifies a Feeds plugin key to be tested against.
- *
- * @return
- *   TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin,
- *   FALSE otherwise.
- */
-function feeds_plugin_child($plugin_key, $parent_plugin) {
-  ctools_include('plugins');
-  $plugins = ctools_get_plugins('feeds', 'plugins');
-  $info = $plugins[$plugin_key];
-
-  if (empty($info['handler']['parent'])) {
-    return FALSE;
-  }
-  elseif ($info['handler']['parent'] == $parent_plugin) {
-    return TRUE;
-  }
-  else {
-    return feeds_plugin_child($info['handler']['parent'], $parent_plugin);
-  }
-}
-
-/**
- * Determines the type of a plugin.
- *
- * @param $plugin_key
- *   String that identifies a Feeds plugin key.
- *
- * @return
- *   One of the following values:
- *   'fetcher' if the plugin is a fetcher
- *   'parser' if the plugin is a parser
- *   'processor' if the plugin is a processor
- *   FALSE otherwise.
- */
-function feeds_plugin_type($plugin_key) {
-  if (feeds_plugin_child($plugin_key, 'FeedsFetcher')) {
-    return 'fetcher';
-  }
-  elseif (feeds_plugin_child($plugin_key, 'FeedsParser')) {
-    return 'parser';
-  }
-  elseif (feeds_plugin_child($plugin_key, 'FeedsProcessor')) {
-    return 'processor';
-  }
-  return FALSE;
-}
-
-/**
  * @}
  */
 
@@ -878,11 +896,13 @@ function feeds_include_library($file, $l
   if (!isset($included[$file])) {
     // Try first whether libraries module is present and load the file from
     // there. If this fails, require the library from the local path.
-    if (module_exists('libraries') && file_exists(libraries_get_path($library) ."/$file")) {
+    if (module_exists('libraries') && file_exists(libraries_get_path($library) . "/$file")) {
       require libraries_get_path($library) ."/$file";
     }
     else {
-      require './'. drupal_get_path('module', 'feeds') ."/libraries/$file";
+      // @todo: Throws "Deprecated function: Assigning the return value of new
+      // by reference is deprecated."
+      require drupal_get_path('module', 'feeds') . "/libraries/$file";
     }
   }
   $included[$file] = TRUE;
@@ -899,15 +919,42 @@ function feeds_include_library($file, $l
  *   libraries module.
  */
 function feeds_library_exists($file, $library) {
-  if (module_exists('libraries') && file_exists(libraries_get_path($library) ."/$file")) {
+  if (module_exists('libraries') && file_exists(libraries_get_path($library) . "/$file")) {
     return TRUE;
   }
-  elseif (file_exists(drupal_get_path('module', 'feeds') ."/libraries/$file")) {
+  elseif (file_exists(drupal_get_path('module', 'feeds') . "/libraries/$file")) {
     return TRUE;
   }
   return FALSE;
 }
 
 /**
+ * Simplified drupal_alter().
+ *
+ * - None of that 'multiple parameters by ref' crazyness.
+ * - Don't use module_implements() to allow hot including on behalf
+ *   implementations (see mappers/).
+ */
+function feeds_alter($type, &$data) {
+  $args = array(&$data);
+  $additional_args = func_get_args();
+  array_shift($additional_args);
+  array_shift($additional_args);
+  $args = array_merge($args, $additional_args);
+
+  $list = module_list();
+  $path = drupal_get_path('module', 'feeds') .'/mappers/';
+  foreach (module_list() as $module) {
+    if (file_exists($path . $module . '.inc')) {
+      include_once $path . $module . '.inc';
+    }
+    $function = $module .'_'. $type .'_alter';
+    if (function_exists($function)) {
+      call_user_func_array($function, $args);
+    }
+  }
+}
+
+/**
  * @}
  */
Index: feeds.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.pages.inc,v
retrieving revision 1.22.2.1
diff -u -p -r1.22.2.1 feeds.pages.inc
--- feeds.pages.inc	25 Oct 2010 22:14:58 -0000	1.22.2.1
+++ feeds.pages.inc	3 Feb 2011 13:03:54 -0000
@@ -33,6 +33,9 @@ function feeds_page() {
       );
     }
   }
+  if (empty($rows)) {
+    drupal_set_message(t('There are no importers, go to !importers to create one or enable an existing one.', array('!importers' => l(t('Feeds importers'), 'admin/structure/feeds'))));
+  }
   $header = array(
     t('Import'),
     t('Description'),
@@ -50,8 +53,15 @@ function feeds_import_form(&$form_state,
   $form['#importer_id'] = $importer_id;
   // @todo Move this into fetcher?
   $form['#attributes']['enctype'] = 'multipart/form-data';
+  $form['source_status'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Status'),
+    '#tree' => TRUE,
+    '#value' => feeds_source_status($source),
+  );
   $form['feeds'] = array(
     '#type' => 'fieldset',
+    '#title' => t('Import'),
     '#tree' => TRUE,
   );
   $form['feeds'] += $source->configForm($form_state);
@@ -59,6 +69,12 @@ function feeds_import_form(&$form_state,
     '#type' => 'submit',
     '#value' => t('Import'),
   );
+  $progress = $source->progressImporting();
+  if ($progress !== FEEDS_BATCH_COMPLETE) {
+    $form['submit']['#disabled'] = TRUE;
+    $form['submit']['#value'] =
+      t('Importing (@progress %)', array('@progress' => number_format(100 * $progress, 0)));
+  }
   return $form;
 }
 
@@ -82,7 +98,7 @@ function feeds_import_form_submit($form,
 
   // Refresh feed if import on create is selected.
   if ($source->importer->config['import_on_create']) {
-    feeds_batch_set(t('Importing'), 'import', $form['#importer_id']);
+    $source->startImport();
   }
 
   // Add to schedule, make sure importer is scheduled, too.
@@ -95,12 +111,26 @@ function feeds_import_form_submit($form,
  */
 function feeds_import_tab_form(&$form_state, $node) {
   $importer_id = feeds_get_importer_id($node->type);
+  $source = feeds_source($importer_id, $node->nid);
 
   $form = array();
   $form['#feed_nid'] = $node->nid;
   $form['#importer_id'] = $importer_id;
   $form['#redirect'] = 'node/'. $node->nid;
-  return confirm_form($form, t('Import all content from feed?'), 'node/'. $node->nid, '', t('Import'), t('Cancel'), 'confirm feeds update');
+  $form['source_status'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Status'),
+    '#tree' => TRUE,
+    '#value' => feeds_source_status($source),
+  );
+  $form = confirm_form($form, t('Import all content from source?'), 'node/'. $node->nid, '', t('Import'), t('Cancel'), 'confirm feeds update');
+  $progress = $source->progressImporting();
+  if ($progress !== FEEDS_BATCH_COMPLETE) {
+    $form['actions']['submit']['#disabled'] = TRUE;
+    $form['actions']['submit']['#value'] =
+      t('Importing (@progress %)', array('@progress' => number_format(100 * $progress, 0)));
+  }
+  return $form;
 }
 
 /**
@@ -108,7 +138,7 @@ function feeds_import_tab_form(&$form_st
  */
 function feeds_import_tab_form_submit($form, &$form_state) {
   $form_state['redirect'] = $form['#redirect'];
-  feeds_batch_set(t('Importing'), 'import', $form['#importer_id'], $form['#feed_nid']);
+  feeds_source($form['#importer_id'], $form['#feed_nid'])->startImport();
 }
 
 /**
@@ -119,16 +149,31 @@ function feeds_import_tab_form_submit($f
  */
 function feeds_delete_tab_form(&$form_state, $importer_id, $node = NULL) {
   if (empty($node)) {
-    $form['#redirect'] = 'import/'. $importer_id;
+    $source = feeds_source($importer_id);
+    $form['#redirect'] = 'import/' . $source->id;
   }
   else {
     $importer_id = feeds_get_importer_id($node->type);
-    $form['#feed_nid'] = $node->nid;
-    $form['#redirect'] = 'node/'. $node->nid;
+    $source = feeds_source($importer_id, $node->nid);
+    $form['#redirect'] = 'node/' . $source->feed_nid;
   }
-  // Form cannot pass on feed object.
-  $form['#importer_id'] = $importer_id;
-  return confirm_form($form, t('Delete all items from feed?'), $form['#redirect'], '', t('Delete'), t('Cancel'), 'confirm feeds update');
+  // Form cannot pass on source object.
+  $form['#importer_id'] = $source->id;
+  $form['#feed_nid'] = $source->feed_nid;
+  $form['source_status'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Status'),
+    '#tree' => TRUE,
+    '#value' => feeds_source_status($source),
+  );
+  $form = confirm_form($form, t('Delete all items from source?'), $form['#redirect'], '', t('Delete'), t('Cancel'), 'confirm feeds update');
+  $progress = $source->progressClearing();
+  if ($progress !== FEEDS_BATCH_COMPLETE) {
+    $form['actions']['submit']['#disabled'] = TRUE;
+    $form['actions']['submit']['#value'] =
+      t('Deleting (@progress %)', array('@progress' => number_format(100 * $progress, 0)));
+  }
+  return $form;
 }
 
 /**
@@ -137,7 +182,7 @@ function feeds_delete_tab_form(&$form_st
 function feeds_delete_tab_form_submit($form, &$form_state) {
   $form_state['redirect'] = $form['#redirect'];
   $feed_nid = empty($form['#feed_nid']) ? 0 : $form['#feed_nid'];
-  feeds_batch_set(t('Deleting'), 'clear', $form['#importer_id'], $feed_nid);
+  feeds_source($form['#importer_id'], $feed_nid)->startClear();
 }
 
 /**
@@ -154,30 +199,96 @@ function feeds_fetcher_callback($importe
 }
 
 /**
+ * Template generation
+ */
+function feeds_importer_template($importer_id) {
+  $importer = feeds_importer($importer_id);
+  if ($importer->parser instanceof FeedsCSVParser) {
+    // @todo
+    //return $importer->parser->getTemplate();
+  }
+  return drupal_not_found();
+}
+
+/**
+ * Renders a status display for a source.
+ */
+function feeds_source_status($source) {
+  $progress_importing = $source->progressImporting();
+  $v = array();
+  $v['progress_importing'] = FALSE;
+  $v['progress_clearing'] = FALSE;
+  if ($progress_importing != FEEDS_BATCH_COMPLETE) {
+    $v['progress_importing'] = $progress_importing;
+  }
+  $progress_clearing = $source->progressClearing();
+  if ($progress_clearing != FEEDS_BATCH_COMPLETE) {
+    $v['progress_clearing'] = $progress_clearing;
+  }
+  $v['imported'] = $source->imported;
+  $v['count'] = $source->itemCount();
+  if (!empty($v)) {
+    return theme('feeds_source_status', $v);
+  }
+}
+
+/**
+ * Themes a status display for a source.
+ */
+function theme_feeds_source_status($v) {
+  $output = '<div class="info-box feeds-source-status">';
+  $items = array();
+  if ($v['progress_importing']) {
+    $progress = number_format(100.0 * $v['progress_importing'], 0);
+    $items[] = t('Importing - @progress % complete.', array('@progress' => $progress));
+  }
+  if ($v['progress_clearing']) {
+    $progress = number_format(100.0 * $v['progress_clearing'], 0);
+    $items[] = t('Deleting items - @progress % complete.', array('@progress' => $progress));
+  }
+  if (!count($items)) {
+    if ($v['count']) {
+      if ($v['imported']) {
+        $items[] = t('Last import: @ago ago.', array('@ago' => format_interval(FEEDS_REQUEST_TIME - $v['imported'], 1)));
+      }
+      $items[] = t('@count imported items total.', array('@count' => $v['count']));
+    }
+    else {
+      $items[] = t('No imported items.');
+    }
+  }
+  $output .= theme('item_list', $items);
+  $output .= '</div>';
+  return $output;
+}
+
+/**
  * Theme upload widget.
  */
 function theme_feeds_upload($element) {
   drupal_add_css(drupal_get_path('module', 'feeds') .'/feeds.css');
   _form_set_class($element, array('form-file'));
-  $output = '';
+  $description = '';
   if (!empty($element['#file_info'])) {
     $info = $element['#file_info'];
-    $output .= '<div class="file-info">';
-    $output .= '<div class="file-name">';
-    $output .= l(basename($info['path']), $info['path']);
-    $output .= '</div>';
-    $output .= '<div class="file-size">';
-    $output .= format_size($info['size']);
-    $output .= '</div>';
+    $description .= '<div class="file-info">';
+    $description .= '<div class="file-name">';
+    $description .= l(basename($info['filepath']), $info['filepath']);
+    $description .= '</div>';
+    $description .= '<div class="file-size">';
+    $description .= format_size($info['filesize']);
+    $description .= '</div>';
     if (isset($info['mime'])) {
-      $output .= '<div class="file-mime">';
-      $output .= check_plain($info['mime']);
-      $output .= '</div>';
+      $description .= '<div class="file-mime">';
+      $description .= check_plain($info['filemime']);
+      $description .= '</div>';
     }
-    $output .= '</div>';
   }
-  $output .= '<div class="file-upload">';
-  $output .= '<input type="file" name="'. $element['#name'] .'"'. ($element['#attributes'] ? ' '. drupal_attributes($element['#attributes']) : '') .' id="'. $element['#id'] .'" size="'. $element['#size'] ."\" />\n";
-  $output .= '</div>';
-  return theme('form_element', $element, $output);
+  $description .= '<div class="file-upload">';
+  $description .= '<input type="file" name="'. $element['#name'] .'"'. ($element['#attributes'] ? ' '. drupal_attributes($element['#attributes']) : '') .' id="'. $element['#id'] .'" size="'. $element['#size'] ."\" />\n";
+  $description .= '</div>';
+
+  // For some reason not unsetting #title leads to printing the title twice.
+  unset($element['#title']);
+  return theme('form_element', $element, $description);
 }
Index: feeds.plugins.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.plugins.inc,v
retrieving revision 1.6
diff -u -p -r1.6 feeds.plugins.inc
--- feeds.plugins.inc	10 Jul 2010 22:43:05 -0000	1.6
+++ feeds.plugins.inc	3 Feb 2011 13:03:54 -0000
@@ -141,17 +141,6 @@ function _feeds_feeds_plugins() {
       'path' => $path,
     ),
   );
-  $info['FeedsFeedNodeProcessor'] = array(
-    'name' => 'Feed Node processor',
-    'description' => 'Create <em>Feed nodes</em>.',
-    'help' => 'Create <em>Feed nodes</em> from parsed content. Feed nodes are nodes that can import feeds themselves. This can be useful for instance when importing OPML feeds.',
-    'handler' => array(
-      'parent' => 'FeedsProcessor',
-      'class' => 'FeedsFeedNodeProcessor',
-      'file' => 'FeedsFeedNodeProcessor.inc',
-      'path' => $path,
-    ),
-  );
   $info['FeedsUserProcessor'] = array(
     'name' => 'User processor',
     'description' => 'Create users.',
Index: feeds_import/feeds_import.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_import/feeds_import.test,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_import.test
--- feeds_import/feeds_import.test	15 Sep 2010 19:27:42 -0000	1.2
+++ feeds_import/feeds_import.test	3 Feb 2011 13:03:54 -0000
@@ -48,7 +48,7 @@ class FeedsExamplesNodeTestCase extends 
     $this->importFile('node', $this->absolutePath() .'/tests/feeds/nodes.csv');
 
     // Assert returning page.
-    $this->assertText('Created 8 Story nodes.');
+    $this->assertText('Created 8 nodes.');
     $this->assertText('Import CSV files with one or more of these columns: title, body, published, guid.');
     $this->assertText('Column guid is mandatory and considered unique: only one item per guid value will be created.');
     $this->assertRaw('feeds/nodes.csv');
@@ -72,7 +72,7 @@ class FeedsExamplesNodeTestCase extends 
 
     // Assert DB status as is and again after an additional import.
     for ($i = 0; $i < 2; $i++) {
-      $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+      $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
       $this->assertEqual($count, 8, 'Found correct number of items.');
       $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story' AND status = 1 AND uid = 0"));
       $this->assertEqual($count, 8, 'Found correct number of items.');
@@ -84,7 +84,7 @@ class FeedsExamplesNodeTestCase extends 
       // there are 2 different items with the same GUID in nodes.csv.
       // Therefore, feeds will show updates to 2 nodes.
       $this->drupalPost('import/node/import', array(), 'Import');
-      $this->assertText('Updated 2 Story nodes.');
+      $this->assertText('Updated 2 nodes.');
     }
 
     // Remove all nodes.
@@ -93,17 +93,17 @@ class FeedsExamplesNodeTestCase extends 
 
     // Import once again.
     $this->drupalPost('import/node/import', array(), 'Import');
-    $this->assertText('Created 8 Story nodes.');
+    $this->assertText('Created 8 nodes.');
 
     // Import a similar file with changes in 4 records. Feeds should report 6
     // Updated story nodes (4 changed records, 2 records sharing a GUID
     // subsequently being updated).
     $this->importFile('node', $this->absolutePath() .'/tests/feeds/nodes_changes.csv');
-    $this->assertText('Updated 6 Story nodes.');
+    $this->assertText('Updated 6 nodes.');
 
     // Import a larger file with more records.
     $this->importFile('node', $this->absolutePath() .'/tests/feeds/many_nodes.csv');
-    $this->assertText('Created 71 Story nodes.');
+    $this->assertText('Created 71 nodes.');
 
     // Remove all nodes.
     $this->drupalPost('import/node/delete-items', array(), 'Delete');
@@ -111,7 +111,7 @@ class FeedsExamplesNodeTestCase extends 
 
     // Import once again.
     $this->drupalPost('import/node/import', array(), 'Import');
-    $this->assertText('Created 79 Story nodes.');
+    $this->assertText('Created 79 nodes.');
 
     // Import a tab separated file.
     $this->drupalPost('import/node/delete-items', array(), 'Delete');
@@ -120,7 +120,7 @@ class FeedsExamplesNodeTestCase extends 
       'feeds[FeedsCSVParser][delimiter]' => "TAB",
     );
     $this->drupalPost('import/node', $edit, 'Import');
-    $this->assertText('Created 8 Story nodes.');
+    $this->assertText('Created 8 nodes.');
   }
 }
 
@@ -165,7 +165,7 @@ class FeedsExamplesUserTestCase extends 
     // Assert result.
     $this->assertText('Created 4 users.');
     // 1 user has an invalid email address.
-    $this->assertText('There was 1 user that could not be imported because either their name or their email was empty or not valid. Check import data and mapping settings on User processor.');
+    $this->assertText('Failed importing 1 user.');
     $this->drupalGet('admin/user/user');
     $this->assertText('Morticia');
     $this->assertText('Fester');
Index: feeds_news/feeds_news.feeds_importer_default.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_news/feeds_news.feeds_importer_default.inc,v
retrieving revision 1.1
diff -u -p -r1.1 feeds_news.feeds_importer_default.inc
--- feeds_news/feeds_news.feeds_importer_default.inc	27 Jul 2010 23:01:25 -0000	1.1
+++ feeds_news/feeds_news.feeds_importer_default.inc	3 Feb 2011 13:03:54 -0000
@@ -87,7 +87,7 @@ function feeds_news_feeds_importer_defau
       'config' => array(),
     ),
     'processor' => array(
-      'plugin_key' => 'FeedsFeedNodeProcessor',
+      'plugin_key' => 'FeedsNodeProcessor',
       'config' => array(
         'content_type' => 'feed',
         'update_existing' => 0,
Index: feeds_news/feeds_news.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_news/feeds_news.test,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_news.test
--- feeds_news/feeds_news.test	15 Sep 2010 19:27:42 -0000	1.2
+++ feeds_news/feeds_news.test	3 Feb 2011 13:03:54 -0000
@@ -50,8 +50,8 @@ class FeedsExamplesFeedTestCase extends 
     $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'feed_item'"));
     $this->assertEqual($count, 10, 'Found the correct number of feed item nodes in database.');
 
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
-    $this->assertEqual($count, 10, 'Found the correct number of records in feeds_node_item.');
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
+    $this->assertEqual($count, 10, 'Found the correct number of records in feeds_item.');
 
     $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE title = 'Open Atrium Translation Workflow: Two Way Translation Updates'"));
     $this->assertEqual($count, 1, 'Found title.');
@@ -62,13 +62,13 @@ class FeedsExamplesFeedTestCase extends 
     $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE title = 'Scaling the Open Atrium UI'"));
     $this->assertEqual($count, 1, 'Found title.');
 
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item} WHERE url = 'http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating'"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} WHERE url = 'http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating'"));
     $this->assertEqual($count, 1, 'Found feed_node_item record.');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item} WHERE url = 'http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition'"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} WHERE url = 'http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition'"));
     $this->assertEqual($count, 1, 'Found feed_node_item record.');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item} WHERE guid = '974 at http://developmentseed.org'"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} WHERE guid = '974 at http://developmentseed.org'"));
     $this->assertEqual($count, 1, 'Found feed_node_item record.');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item} WHERE guid = '970 at http://developmentseed.org'"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} WHERE guid = '970 at http://developmentseed.org'"));
     $this->assertEqual($count, 1, 'Found feed_node_item record.');
 
     // Remove all items
@@ -77,7 +77,7 @@ class FeedsExamplesFeedTestCase extends 
 
     // Import again.
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('Created 10 Feed item nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // Delete and assert all items gone.
     $this->drupalPost('node/'. $nid .'/delete-items', array(), 'Delete');
@@ -85,15 +85,15 @@ class FeedsExamplesFeedTestCase extends 
     $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'feed_item'"));
     $this->assertEqual($count, 0, 'Found the correct number of feed item nodes in database.');
 
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
-    $this->assertEqual($count, 0, 'Found the correct number of records in feeds_node_item.');
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
+    $this->assertEqual($count, 0, 'Found the correct number of records in feeds_item.');
 
     // Create a batch of nodes.
     $this->createFeedNodes('feed', 10, 'feed');
     $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'feed_item'"));
     $this->assertEqual($count, 100, 'Imported 100 nodes.');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
-    $this->assertEqual($count, 100, 'Found 100 records in feeds_node_item.');
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
+    $this->assertEqual($count, 100, 'Found 100 records in feeds_item.');
   }
 }
 
@@ -136,13 +136,13 @@ class FeedsExamplesOPMLTestCase extends 
     // Import OPML and assert.
     $file = $this->generateOPML();
     $this->importFile('opml', $file);
-    $this->assertText('Created 3 feed nodes.');
+    $this->assertText('Created 3 nodes.');
     $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_source}"));
     $this->assertEqual($count, 4, 'Found correct number of items.');
 
     // Import a feed and then delete all items from it.
     $this->drupalPost('node/1/import', array(), 'Import');
-    $this->assertText('Created 10 Feed item nodes.');
+    $this->assertText('Created 10 nodes.');
     $this->drupalPost('node/1/delete-items', array(), 'Delete');
     $this->assertText('Deleted 10 nodes.');
   }
Index: feeds_news/feeds_news.views_default.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_news/feeds_news.views_default.inc,v
retrieving revision 1.1
diff -u -p -r1.1 feeds_news.views_default.inc
--- feeds_news/feeds_news.views_default.inc	27 Jul 2010 23:01:25 -0000	1.1
+++ feeds_news/feeds_news.views_default.inc	3 Feb 2011 13:03:55 -0000
@@ -22,7 +22,7 @@ function feeds_news_views_default_views(
       'label' => 'Owner feed',
       'required' => 1,
       'id' => 'feed_nid',
-      'table' => 'feeds_node_item',
+      'table' => 'feeds_item',
       'field' => 'feed_nid',
       'override' => array(
         'button' => 'Override',
@@ -57,7 +57,7 @@ function feeds_news_views_default_views(
       'display_as_link' => 1,
       'exclude' => 1,
       'id' => 'url',
-      'table' => 'feeds_node_item',
+      'table' => 'feeds_item',
       'field' => 'url',
       'override' => array(
         'button' => 'Override',
Index: feeds_ui/feeds_ui.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_ui/feeds_ui.admin.inc,v
retrieving revision 1.39
diff -u -p -r1.39 feeds_ui.admin.inc
--- feeds_ui/feeds_ui.admin.inc	16 Sep 2010 19:01:04 -0000	1.39
+++ feeds_ui/feeds_ui.admin.inc	3 Feb 2011 13:03:55 -0000
@@ -274,7 +274,7 @@ function feeds_ui_export_form(&$form_sta
 function feeds_ui_edit_page($importer, $active = 'help', $plugin_key = '') {
 
   // Get plugins and configuration.
-  $plugins = feeds_get_plugins();
+  $plugins = FeedsPlugin::all();
   $config = $importer->config;
   // Base path for changing the active container.
   $path = 'admin/build/feeds/edit/'. $importer->id;
@@ -304,7 +304,7 @@ function feeds_ui_edit_page($importer, $
       }
       // feeds_plugin_instance() returns a correct result because feed has been
       // instantiated previously.
-      elseif (in_array($plugin_key, array_keys($plugins)) && $plugin = feeds_plugin_instance($plugin_key, $importer->id)) {
+      elseif (in_array($plugin_key, array_keys($plugins)) && $plugin = feeds_plugin($plugin_key, $importer->id)) {
         $active_container['title'] = t('Settings for !plugin', array('!plugin' => $plugins[$plugin_key]['name']));
         $active_container['body'] = feeds_get_form($plugin, 'configForm');
       }
@@ -412,7 +412,7 @@ function feeds_ui_edit_page($importer, $
  *   A Form API form definition.
  */
 function feeds_ui_plugin_form(&$form_state, $importer, $type) {
-  $plugins = feeds_get_plugins_by_type($type);
+  $plugins = FeedsPlugin::byType($type);
 
   $form = array();
   $form['#importer'] = $importer;
@@ -486,7 +486,7 @@ function feeds_ui_mapping_form(&$form_st
   $form['#importer'] = $importer;
   $form['#mappings'] = $mappings = $importer->processor->getMappings();
   $form['help']['#value'] = feeds_ui_mapping_help();
-
+  $source_options = NULL;
   // Get mapping sources from parsers and targets from processor, format them
   // for output.
   // Some parsers do not define mapping sources but let them define on the fly.
Index: feeds_ui/feeds_ui.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_ui/feeds_ui.module,v
retrieving revision 1.11
diff -u -p -r1.11 feeds_ui.module
--- feeds_ui/feeds_ui.module	10 Jul 2010 23:00:20 -0000	1.11
+++ feeds_ui/feeds_ui.module	3 Feb 2011 13:03:55 -0000
@@ -1,5 +1,6 @@
 <?php
 // $Id: feeds_ui.module,v 1.11 2010/07/10 23:00:20 alexb Exp $
+
 /**
  * @file
  */
@@ -12,9 +13,6 @@ function feeds_ui_help($path, $arg) {
     case 'admin/build/feeds':
       $output = '<p>'. t('Create one or more Feed importers for pulling content into Drupal. You can use these importers from the !import page or - if you attach them to a content type - simply by creating a node from that content type.', array('!import' => l(t('Import'), 'import'))) .'</p>';
       return $output;
-    case 'admin/content/feeds':
-      $output = '<p>'. t('Import content into Drupal.') .'</p>';
-      return $output;
   }
 }
 
@@ -24,8 +22,8 @@ function feeds_ui_help($path, $arg) {
 function feeds_ui_menu() {
   $items = array();
   $items['admin/build/feeds'] = array(
-    'title' => 'Feed importers',
-    'description' => 'Configure feeds to import or aggregate RSS and Atom feeds, import CSV files or more.',
+    'title' => 'Feeds importers',
+    'description' => 'Configure one or more Feeds importers to aggregate RSS and Atom feeds, import CSV files or more.',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('feeds_ui_overview_form'),
     'access arguments' => array('administer feeds'),
Index: feeds_ui/tests/feeds_ui.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_ui/tests/feeds_ui.test,v
retrieving revision 1.7
diff -u -p -r1.7 feeds_ui.test
--- feeds_ui/tests/feeds_ui.test	15 Sep 2010 19:27:42 -0000	1.7
+++ feeds_ui/tests/feeds_ui.test	3 Feb 2011 13:03:56 -0000
@@ -89,12 +89,12 @@ class FeedsUIUserInterfaceTestCase exten
     $this->clickLink('Change', 2);
     $this->assertText('Select a processor');
     $edit = array(
-      'plugin_key' => 'FeedsFeedNodeProcessor',
+      'plugin_key' => 'FeedsUserProcessor',
     );
     $this->drupalPost('admin/build/feeds/edit/test_feed/processor', $edit, 'Save');
 
     // Assert changed configuration.
-    $this->assertPlugins('test_feed', 'FeedsFileFetcher', 'FeedsCSVParser', 'FeedsFeedNodeProcessor');
+    $this->assertPlugins('test_feed', 'FeedsFileFetcher', 'FeedsCSVParser', 'FeedsUserProcessor');
 
     // Delete feed.
     $this->drupalPost('admin/build/feeds/delete/test_feed', array(), 'Delete');
@@ -136,4 +136,4 @@ class FeedsUIUserInterfaceTestCase exten
 
     // @todo Refreshing/deleting feed items. Needs to live in feeds.test
   }
-}
\ No newline at end of file
+}
Index: includes/FeedsConfigurable.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/includes/FeedsConfigurable.inc,v
retrieving revision 1.14.2.2
diff -u -p -r1.14.2.2 FeedsConfigurable.inc
--- includes/FeedsConfigurable.inc	28 Oct 2010 20:07:59 -0000	1.14.2.2
+++ includes/FeedsConfigurable.inc	3 Feb 2011 13:03:56 -0000
@@ -47,7 +47,7 @@ abstract class FeedsConfigurable {
   /**
    * Instantiate a FeedsConfigurable object.
    *
-   * Don't use directly, use feeds_importer() or feeds_plugin_instance()
+   * Don't use directly, use feeds_importer() or feeds_plugin()
    * instead.
    */
   public static function instance($class, $id) {
@@ -112,6 +112,7 @@ abstract class FeedsConfigurable {
    *   values that are not included in $config.
    */
   public function setConfig($config) {
+    $config = (array) $config;
     $defaults = $this->configDefaults();
     $this->config = array_intersect_key($config, $defaults) + $defaults;
   }
@@ -124,6 +125,7 @@ abstract class FeedsConfigurable {
    *   returned by configDefaults().
    */
   public function addConfig($config) {
+    $config = (array) $config;
     $this->config = is_array($this->config) ? array_merge($this->config, $config) : $config;
     $default_keys = $this->configDefaults();
     $this->config = array_intersect_key($this->config, $default_keys);
@@ -137,14 +139,17 @@ abstract class FeedsConfigurable {
     if ($name == 'config') {
       return $this->getConfig();
     }
-    return $this->{$name};
+    return isset($this->$name) ? $this->$name : NULL;
   }
 
   /**
    * Implementation of getConfig().
+   *
+   * Return configuration array, ensure that all default values are present.
    */
   public function getConfig() {
-    return $this->config;
+    $defaults = $this->configDefaults();
+    return $this->config + $defaults;
   }
 
   /**
@@ -205,14 +210,12 @@ abstract class FeedsConfigurable {
  *   The form method that should be rendered.
  *
  * @return
- *   Rendered config form, if available. Empty string otherwise.
+ *   Config form array if available. NULL otherwise.
  */
 function feeds_get_form($configurable, $form_method) {
-  $form_state = array();
   if (method_exists($configurable, $form_method)) {
     return drupal_get_form(get_class($configurable) .'_feeds_form', $configurable, $form_method);
   }
-  return '';
 }
 
 /**
Index: includes/FeedsImporter.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/includes/FeedsImporter.inc,v
retrieving revision 1.23.2.1
diff -u -p -r1.23.2.1 FeedsImporter.inc
--- includes/FeedsImporter.inc	17 Sep 2010 13:50:04 -0000	1.23.2.1
+++ includes/FeedsImporter.inc	3 Feb 2011 13:03:56 -0000
@@ -9,7 +9,6 @@
 // Including FeedsImporter.inc automatically includes dependencies.
 require_once(dirname(__FILE__) .'/FeedsConfigurable.inc');
 require_once(dirname(__FILE__) .'/FeedsSource.inc');
-require_once(dirname(__FILE__) .'/FeedsBatch.inc');
 
 /**
  * A FeedsImporter object describes how an external source should be fetched,
@@ -32,7 +31,7 @@ class FeedsImporter extends FeedsConfigu
 
   // Every feed has a fetcher, a parser and a processor.
   // These variable names match the possible return values of
-  // feeds_plugin_type().
+  // FeedsPlugin::typeOf().
   protected $fetcher, $parser, $processor;
 
   // This array defines the variable names of the plugins above.
@@ -51,7 +50,7 @@ class FeedsImporter extends FeedsConfigu
     // Instantiate fetcher, parser and processor, set their configuration if
     // stored info is available.
     foreach ($this->plugin_types as $type) {
-      $plugin = feeds_plugin_instance($this->config[$type]['plugin_key'], $this->id);
+      $plugin = feeds_plugin($this->config[$type]['plugin_key'], $this->id);
 
       if (isset($this->config[$type]['config'])) {
         $plugin->setConfig($this->config[$type]['config']);
@@ -79,9 +78,16 @@ class FeedsImporter extends FeedsConfigu
   }
 
   /**
-   * Schedule this importer.
+   * Schedule all periodic tasks for this importer.
    */
   public function schedule() {
+    $this->scheduleExpire();
+  }
+
+  /**
+   * Schedule expiry of items.
+   */
+  public function scheduleExpire() {
     $job = array(
       'callback' => 'feeds_importer_expire',
       'type' => $this->id,
@@ -98,6 +104,25 @@ class FeedsImporter extends FeedsConfigu
   }
 
   /**
+   * Report how many items *should* be created on one page load by this
+   * importer.
+   *
+   * Note:
+   *
+   * It depends on whether parser implements batching if this limit is actually
+   * respected. Further, if no limit is reported it doesn't mean that the
+   * number of items that can be created on one page load is actually without
+   * limit.
+   *
+   * @return
+   *   A positive number defining the number of items that can be created on
+   *   one page load. 0 if this number is unlimited.
+   */
+  public function getLimit() {
+    return $this->processor->getLimit();
+  }
+
+  /**
    * Save configuration.
    */
   public function save() {
@@ -158,8 +183,8 @@ class FeedsImporter extends FeedsConfigu
    */
   public function setPlugin($plugin_key) {
     // $plugin_type can be either 'fetcher', 'parser' or 'processor'
-    if ($plugin_type = feeds_plugin_type($plugin_key)) {
-      if ($plugin = feeds_plugin_instance($plugin_key, $this->id)) {
+    if ($plugin_type = FeedsPlugin::typeOf($plugin_key)) {
+      if ($plugin = feeds_plugin($plugin_key, $this->id)) {
         // Unset existing plugin, switch to new plugin.
         unset($this->$plugin_type);
         $this->$plugin_type = $plugin;
@@ -176,16 +201,18 @@ class FeedsImporter extends FeedsConfigu
    * @param FeedsImporter $importer
    *   The feeds importer object to copy from.
    */
-  public function copy(FeedsImporter $importer) {
-    $this->setConfig($importer->config);
+   public function copy(FeedsConfigurable $configurable) {
+     parent::copy($configurable);
 
-    // Instantiate new fetcher, parser and processor and initialize their
-    // configurations.
-    foreach ($this->plugin_types as $plugin_type) {
-      $this->setPlugin($importer->config[$plugin_type]['plugin_key']);
-      $this->$plugin_type->setConfig($importer->config[$plugin_type]['config']);
-    }
-  }
+     if ($configurable instanceof FeedsImporter) {
+       // Instantiate new fetcher, parser and processor and initialize their
+       // configurations.
+       foreach ($this->plugin_types as $plugin_type) {
+         $this->setPlugin($configurable->config[$plugin_type]['plugin_key']);
+         $this->$plugin_type->setConfig($configurable->config[$plugin_type]['config']);
+       }
+     }
+   }
 
   /**
    * Get configuration of this feed.
@@ -194,7 +221,7 @@ class FeedsImporter extends FeedsConfigu
     foreach (array('fetcher', 'parser', 'processor') as $type) {
       $this->config[$type]['config'] = $this->$type->getConfig();
     }
-    return $this->config;// Collect information from plugins.
+    return parent::getConfig();
   }
 
   /**
@@ -217,7 +244,8 @@ class FeedsImporter extends FeedsConfigu
       'update' => 0,
       'import_period' => 1800, // Refresh every 30 minutes by default.
       'expire_period' => 3600, // Expire every hour by default, this is a hidden setting.
-      'import_on_create' => TRUE, // Import on create.
+      'import_on_create' => TRUE, // Import on submission.
+      'process_in_background' => FALSE,
     );
   }
 
@@ -225,42 +253,59 @@ class FeedsImporter extends FeedsConfigu
    * Override parent::configForm().
    */
   public function configForm(&$form_state) {
+    $config = $this->getConfig();
     $form = array();
     $form['name'] = array(
       '#type' => 'textfield',
       '#title' => t('Name'),
-      '#description' => t('The name of this configuration.'),
-      '#default_value' => $this->config['name'],
+      '#description' => t('A human readable name of this importer.'),
+      '#default_value' => $config['name'],
       '#required' => TRUE,
     );
     $form['description'] = array(
       '#type' => 'textfield',
       '#title' => t('Description'),
-      '#description' => t('A description of this configuration.'),
-      '#default_value' => $this->config['description'],
+      '#description' => t('A description of this importer.'),
+      '#default_value' => $config['description'],
     );
+    $node_types = node_get_types('names');
+    array_walk($node_types, 'check_plain');
     $form['content_type'] = array(
       '#type' => 'select',
       '#title' => t('Attach to content type'),
-      '#description' => t('If an importer is attached to a content type, content is imported by creating a node. If the standalone form is selected, content is imported by using the standalone form under http://example.com/import.'),
-      '#options' => array('' => t('Use standalone form')) + node_get_types('names'),
-      '#default_value' => $this->config['content_type'],
+      '#description' => t('If "Use standalone form" is selected a source is imported by using a form under !import_form.
+                           If a content type is selected a source is imported by creating a node of that content type.',
+                           array('!import_form' => l(url('import', array('absolute' => TRUE)), 'import', array('attributes' => array('target' => '_new'))))),
+      '#options' => array('' => t('Use standalone form')) + $node_types,
+      '#default_value' => $config['content_type'],
     );
-    $period = drupal_map_assoc(array(0, 900, 1800, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2419200), 'format_interval');
-    $period[FEEDS_SCHEDULE_NEVER] = t('Never');
-    $period[0] = t('As often as possible');
+    $cron_required =  ' ' . l(t('Requires cron to be configured.'), 'http://drupal.org/cron', array('attributes' => array('target' => '_new')));
+    $period = drupal_map_assoc(array(900, 1800, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2419200), 'format_interval');
+    foreach ($period as &$p) {
+      $p = t('Every !p', array('!p' => $p));
+    }
+    $period = array(
+      FEEDS_SCHEDULE_NEVER => t('Off'),
+      0 => t('As often as possible'),
+    ) + $period;
     $form['import_period'] = array(
       '#type' => 'select',
-      '#title' => t('Minimum refresh period'),
+      '#title' => t('Periodic import'),
       '#options' => $period,
-      '#description' => t('This is the minimum time that must elapse before a feed may be refreshed automatically.'),
-      '#default_value' => $this->config['import_period'],
+      '#description' => t('Choose how often a source should be imported periodically.') . $cron_required,
+      '#default_value' => $config['import_period'],
     );
     $form['import_on_create'] = array(
       '#type' => 'checkbox',
       '#title' => t('Import on submission'),
-      '#description' => t('Check if content should be imported at the moment of feed submission.'),
-      '#default_value' => $this->config['import_on_create'],
+      '#description' => t('Check if import should be started at the moment a standalone form or node form is submitted.'),
+      '#default_value' => $config['import_on_create'],
+    );
+    $form['process_in_background'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Process in background'),
+      '#description' => t('For very large imports. If checked, import and delete tasks started from the web UI will be handled by a cron task in the background rather than by the browser. This does not affect periodic imports, they are handled by a cron task in any case.') . $cron_required,
+      '#default_value' => $config['process_in_background'],
     );
     return $form;
   }
Index: includes/FeedsSource.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/includes/FeedsSource.inc,v
retrieving revision 1.21.2.1
diff -u -p -r1.21.2.1 FeedsSource.inc
--- includes/FeedsSource.inc	26 Sep 2010 17:39:45 -0000	1.21.2.1
+++ includes/FeedsSource.inc	3 Feb 2011 13:03:57 -0000
@@ -7,6 +7,20 @@
  */
 
 /**
+ * Distinguish exceptions occuring when handling locks.
+ */
+class FeedsLockException extends Exception {}
+
+/**
+ * Denote a import or clearing stage. Used for multi page processing.
+ */
+define('FEEDS_START', 'start_time');
+define('FEEDS_FETCH', 'fetch');
+define('FEEDS_PARSE', 'parse');
+define('FEEDS_PROCESS', 'process');
+define('FEEDS_PROCESS_CLEAR', 'process_clear');
+
+/**
  * Declares an interface for a class that defines default values and form
  * descriptions for a FeedSource.
  */
@@ -52,6 +66,77 @@ interface FeedsSourceInterface {
 }
 
 /**
+ * Status of an import or clearing operation on a source.
+ */
+class FeedsState {
+  /**
+   * Floating point number denoting the progress made. 0.0 meaning no progress
+   * 1.0 = FEEDS_BATCH_COMPLETE meaning finished.
+   */
+  public $progress;
+
+  /**
+   * Used as a pointer to store where left off. Must be serializable.
+   */
+  public $pointer;
+
+  /**
+   * Natural numbers denoting more details about the progress being made.
+   */
+  public $total;
+  public $created;
+  public $updated;
+  public $deleted;
+  public $skipped;
+  public $failed;
+
+  /**
+   * Constructor, initialize variables.
+   */
+  public function __construct() {
+    $this->progress = FEEDS_BATCH_COMPLETE;
+    $this->total =
+    $this->created =
+    $this->updated =
+    $this->deleted =
+    $this->skipped =
+    $this->failed = 0;
+  }
+
+  /**
+   * Safely report progress.
+   *
+   * When $total == $progress, the state of the task tracked by this state is
+   * regarded to be complete.
+   *
+   * Handles the following cases gracefully:
+   *
+   * - $total is 0
+   * - $progress is larger than $total
+   * - $progress approximates $total so that $finished rounds to 1.0
+   *
+   * @param $total
+   *   A natural number that is the total to be worked off.
+   * @param $progress
+   *   A natural number that is the progress made on $total.
+   */
+  public function progress($total, $progress) {
+    if ($progress > $total) {
+      $this->progress = FEEDS_BATCH_COMPLETE;
+    }
+    elseif ($total) {
+      $this->progress = $progress / $total;
+      if ($this->progress == FEEDS_BATCH_COMPLETE && $total != $progress) {
+        $this->progress = 0.99;
+      }
+    }
+    else {
+      $this->progress = FEEDS_BATCH_COMPLETE;
+    }
+  }
+}
+
+/**
  * This class encapsulates a source of a feed. It stores where the feed can be
  * found and how to import it.
  *
@@ -84,8 +169,15 @@ class FeedsSource extends FeedsConfigura
   // The FeedsImporter object that this source is expected to be used with.
   protected $importer;
 
-  // A FeedsBatch object. NULL if there is no active batch.
-  protected $batch;
+  // A FeedsSourceState object holding the current import/clearing state of this
+  // source.
+  protected $state;
+
+  // Fetcher result, used to cache fetcher result when batching.
+  protected $fetcher_result;
+
+  // Timestamp when this source was imported the last time.
+  protected $imported;
 
   /**
    * Instantiate a unique object per class/id/feed_nid. Don't use
@@ -114,22 +206,121 @@ class FeedsSource extends FeedsConfigura
    * Preview = fetch and parse a feed.
    *
    * @return
-   *   FeedsImportBatch object, fetched and parsed.
+   *   FeedsParserResult object.
    *
    * @throws
    *   Throws Exception if an error occurs when fetching or parsing.
    */
   public function preview() {
-    $this->batch = $this->importer->fetcher->fetch($this);
-    $this->importer->parser->parse($this->batch, $this);
-    module_invoke_all('feeds_after_parse', $this->importer, $this);
-    $batch = $this->batch;
-    unset($this->batch);
-    return $batch;
+    $result = $this->importer->fetcher->fetch($this);
+    $result = $this->importer->parser->parse($this, $result);
+    module_invoke_all('feeds_after_parse', $this, $result);
+    return $result;
+  }
+
+  /**
+   * Start importing a source.
+   *
+   * This method starts an import job. Depending on the configuration of the
+   * importer of this source, a Batch API job or a background job with Job
+   * Scheduler will be created.
+   *
+   * @throws Exception
+   *   If processing in background is enabled, the first batch chunk of the
+   *   import will be executed on the current page request. This means that this
+   *   method may throw the same exceptions as FeedsSource::import().
+   */
+  public function startImport() {
+    $config = $this->importer->getConfig();
+    if ($config['process_in_background']) {
+      $this->startBackgroundJob('import');
+    }
+    else {
+      $this->startBatchAPIJob(t('Importing'), 'import');
+    }
+  }
+
+  /**
+   * Start deleting all imported items of a source.
+   *
+   * This method starts a clear job. Depending on the configuration of the
+   * importer of this source, a Batch API job or a background job with Job
+   * Scheduler will be created.
+   *
+   * @throws Exception
+   *   If processing in background is enabled, the first batch chunk of the
+   *   clear task will be executed on the current page request. This means that
+   *   this method may throw the same exceptions as FeedsSource::clear().
+   */
+  public function startClear() {
+    $config = $this->importer->getConfig();
+    if ($config['process_in_background']) {
+      $this->startBackgroundJob('clear');
+    }
+    else {
+      $this->startBatchAPIJob(t('Deleting'), 'clear');
+    }
+  }
+
+  /**
+   * Schedule all periodic tasks for this source.
+   */
+  public function schedule() {
+    $this->scheduleImport();
+  }
+
+  /**
+   * Schedule periodic or background import tasks.
+   */
+  public function scheduleImport() {
+    // Check whether any fetcher is overriding the import period.
+    $period = $this->importer->config['import_period'];
+    $fetcher_period = $this->importer->fetcher->importPeriod($this);
+    if (is_numeric($fetcher_period)) {
+      $period = $fetcher_period;
+    }
+    $period = $this->progressImporting() === FEEDS_BATCH_COMPLETE ? $period : 0;
+    $job = array(
+      'callback' => 'feeds_source_import',
+      'type' => $this->id,
+      'id' => $this->feed_nid,
+      // Schedule as soon as possible if a batch is active.
+      'period' => $period,
+      'periodic' => TRUE,
+    );
+    if ($period != FEEDS_SCHEDULE_NEVER) {
+      job_scheduler()->set($job);
+    }
+    else {
+      job_scheduler()->remove($job);
+    }
   }
 
   /**
-   * Import a feed: execute fetching, parsing and processing stage.
+   * Schedule background clearing tasks.
+   */
+  public function scheduleClear() {
+    // Schedule as soon as possible if batch is not complete.
+    if ($this->progressClearing() !== FEEDS_BATCH_COMPLETE) {
+      $job = array(
+        'callback' => 'feeds_source_clear',
+        'type' => $this->id,
+        'id' => $this->feed_nid,
+        'period' => 0,
+        'periodic' => TRUE,
+      );
+      job_scheduler()->set($job);
+    }
+    else {
+      job_scheduler()->remove($job);
+    }
+  }
+
+  /**
+   * Import a source: execute fetching, parsing and processing stage.
+   *
+   * This method only executes the current batch chunk, then returns. If you are
+   * looking to import an entire source, use FeedsSource::startImport() instead.
    *
    * @return
    *   FEEDS_BATCH_COMPLETE if the import process finished. A decimal between
@@ -139,31 +330,52 @@ class FeedsSource extends FeedsConfigura
    *   Throws Exception if an error occurs when importing.
    */
   public function import() {
+    $this->acquireLock();
     try {
-      if (!$this->batch || !($this->batch instanceof FeedsImportBatch)) {
-        $this->batch = $this->importer->fetcher->fetch($this);
-        $this->importer->parser->parse($this->batch, $this);
-        module_invoke_all('feeds_after_parse', $this->importer, $this);
+      // If fetcher result is empty, we are starting a new import, log.
+      if (empty($this->fetcher_result)) {
+        $this->state[FEEDS_START] = time();
       }
-      $this->importer->processor->process($this->batch, $this);
-      $result = $this->batch->getProgress();
-      if ($result == FEEDS_BATCH_COMPLETE) {
-        unset($this->batch);
-        module_invoke_all('feeds_after_import', $this->importer, $this);
+
+      // Fetch.
+      if (empty($this->fetcher_result) || FEEDS_BATCH_COMPLETE == $this->progressParsing()) {
+        $this->fetcher_result = $this->importer->fetcher->fetch($this);
+        // Clean the parser's state, we are parsing an entirely new file.
+        unset($this->state[FEEDS_PARSE]);
       }
+
+      // Parse.
+      $parser_result = $this->importer->parser->parse($this, $this->fetcher_result);
+      module_invoke_all('feeds_after_parse', $this, $parser_result);
+
+      // Process.
+      $this->importer->processor->process($this, $parser_result);
+    }
+    catch (Exception $e) {}
+    $this->releaseLock();
+
+    // Clean up.
+    $result = $this->progressImporting();
+    if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
+      module_invoke_all('feeds_after_import', $this);
+      $this->imported = time();
+      $this->log('import', 'Imported in !s s', array('!s' => $this->imported - $this->state[FEEDS_START]), WATCHDOG_INFO);
+      unset($this->fetcher_result, $this->state);
     }
-    catch (Exception $e) {
-      unset($this->batch);
-      $this->save();
+    $this->save();
+    if (isset($e)) {
       throw $e;
     }
-    $this->save();
     return $result;
   }
 
   /**
    * Remove all items from a feed.
    *
+   * This method only executes the current batch chunk, then returns. If you are
+   * looking to delete all items of a source, use FeedsSource::startClear()
+   * instead.
+   *
    * @return
    *   FEEDS_BATCH_COMPLETE if the clearing process finished. A decimal between
    *   0.0 and 0.9 periodic if clearing is still in progress.
@@ -172,63 +384,99 @@ class FeedsSource extends FeedsConfigura
    *   Throws Exception if an error occurs when clearing.
    */
   public function clear() {
+    $this->acquireLock();
     try {
       $this->importer->fetcher->clear($this);
       $this->importer->parser->clear($this);
-      if (!$this->batch || !($this->batch instanceof FeedsClearBatch)) {
-        $this->batch = new FeedsClearBatch();
-      }
-      $this->importer->processor->clear($this->batch, $this);
-      $result = $this->batch->getProgress();
-      if ($result == FEEDS_BATCH_COMPLETE) {
-        unset($this->batch);
-        module_invoke_all('feeds_after_clear', $this->importer, $this);
-      }
+      $this->importer->processor->clear($this);
     }
-    catch (Exception $e) {
-      unset($this->batch);
-      $this->save();
-      throw $e;
+    catch (Exception $e) {}
+    $this->releaseLock();
+
+    // Clean up.
+    $result = $this->progressClearing();
+    if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
+      module_invoke_all('feeds_after_clear', $this);
+      unset($this->state);
     }
     $this->save();
+    if (isset($e)) {
+      throw $e;
+    }
     return $result;
   }
 
   /**
-   * Schedule this source.
+   * Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
    */
-  public function schedule() {
-    // Check whether any fetcher is overriding the import period.
-    $period = $this->importer->config['import_period'];
-    $fetcher_period = $this->importer->fetcher->importPeriod($this);
-    if (is_numeric($fetcher_period)) {
-      $period = $fetcher_period;
+  public function progressParsing() {
+    return $this->state(FEEDS_PARSE)->progress;
+  }
+
+  /**
+   * Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
+   */
+  public function progressImporting() {
+    $fetcher = $this->state(FEEDS_FETCH);
+    $parser = $this->state(FEEDS_PARSE);
+    if ($fetcher->progress == FEEDS_BATCH_COMPLETE && $parser->progress == FEEDS_BATCH_COMPLETE) {
+      return FEEDS_BATCH_COMPLETE;
     }
-    $job = array(
-      'callback' => 'feeds_source_import',
-      'type' => $this->id,
-      'id' => $this->feed_nid,
-      // Schedule as soon as possible if a batch is active.
-      'period' => $this->batch ? 0 : $period,
-      'periodic' => TRUE,
-    );
-    if ($job['period'] != FEEDS_SCHEDULE_NEVER) {
-      job_scheduler()->set($job);
+    // Fetching envelops parsing.
+    // @todo: this assumes all fetchers neatly use total. May not be the case.
+    $fetcher_fraction = $fetcher->total ? 1.0 / $fetcher->total : 1.0;
+    $parser_progress = $parser->progress * $fetcher_fraction;
+    $result = $fetcher->progress - $fetcher_fraction + $parser_progress;
+    if ($result == FEEDS_BATCH_COMPLETE) {
+      return 0.99;
     }
-    else {
-      job_scheduler()->remove($job);
+    return $result;
+  }
+
+  /**
+   * Report progress on clearing.
+   */
+  public function progressClearing() {
+    return $this->state(FEEDS_PROCESS_CLEAR)->progress;
+  }
+
+  /**
+   * Return a state object for a given stage. Lazy instantiates new states.
+   *
+   * @todo Rename getConfigFor() accordingly to config().
+   *
+   * @param $stage
+   *   One of FEEDS_FETCH, FEEDS_PARSE, FEEDS_PROCESS or FEEDS_PROCESS_CLEAR.
+   *
+   * @return
+   *   The FeedsState object for the given stage.
+   */
+  public function state($stage) {
+    if (!is_array($this->state)) {
+      $this->state = array();
+    }
+    if (!isset($this->state[$stage])) {
+      $this->state[$stage] = new FeedsState();
     }
+    return $this->state[$stage];
+  }
+
+  /**
+   * Count items imported by this source.
+   */
+  public function itemCount() {
+    return $this->importer->processor->itemCount($this);
   }
 
   /**
    * Save configuration.
    */
   public function save() {
-    $config = $this->getConfig();
     // Alert implementers of FeedsSourceInterface to the fact that we're saving.
     foreach ($this->importer->plugin_types as $type) {
       $this->importer->$type->sourceSave($this);
     }
+    $config = $this->getConfig();
     // Store the source property of the fetcher in a separate column so that we
     // can do fast lookups on it.
     $source = '';
@@ -238,9 +486,11 @@ class FeedsSource extends FeedsConfigura
     $object = array(
       'id' => $this->id,
       'feed_nid' => $this->feed_nid,
+      'imported' => $this->imported,
       'config' => $config,
       'source' => $source,
-      'batch' => isset($this->batch) ? $this->batch : FALSE,
+      'state' => isset($this->state) ? $this->state : FALSE,
+      'fetcher_result' => isset($this->fetcher_result) ? $this->fetcher_result : FALSE,
     );
     if (db_result(db_query_range("SELECT 1 FROM {feeds_source} WHERE id = '%s' AND feed_nid = %d", $this->id, $this->feed_nid, 0, 1))) {
       drupal_write_record('feeds_source', $object, array('id', 'feed_nid'));
@@ -256,13 +506,19 @@ class FeedsSource extends FeedsConfigura
    * @todo Patch CTools to move constants from export.inc to ctools.module.
    */
   public function load() {
-    if ($record = db_fetch_object(db_query("SELECT config, batch FROM {feeds_source} WHERE id = '%s' AND feed_nid = %d", $this->id, $this->feed_nid))) {
+    if ($record = db_fetch_object(db_query("SELECT imported, config, state, fetcher_result FROM {feeds_source} WHERE id = '%s' AND feed_nid = %d", $this->id, $this->feed_nid))) {
       // While FeedsSource cannot be exported, we still use CTool's export.inc
       // export definitions.
       ctools_include('export');
       $this->export_type = EXPORT_IN_DATABASE;
+      $this->imported = $record->imported;
       $this->config = unserialize($record->config);
-      $this->batch = unserialize($record->batch);
+      if (!empty($record->state)) {
+        $this->state = unserialize($record->state);
+      }
+      if (!empty($record->fetcher_result)) {
+        $this->fetcher_result = unserialize($record->fetcher_result);
+      }
     }
   }
 
@@ -305,7 +561,7 @@ class FeedsSource extends FeedsConfigura
   }
 
   /**
-   * Convenience function. Returns the configuration for a specific class.
+   * Returns the configuration for a specific client class.
    *
    * @param FeedsSourceInterface $client
    *   An object that is an implementer of FeedsSourceInterface.
@@ -318,6 +574,21 @@ class FeedsSource extends FeedsConfigura
   }
 
   /**
+   * Sets the configuration for a specific client class.
+   *
+   * @param FeedsSourceInterface $client
+   *   An object that is an implementer of FeedsSourceInterface.
+   * @param $config
+   *   The configuration for $client.
+   *
+   * @return
+   *   An array stored for $client.
+   */
+  public function setConfigFor(FeedsSourceInterface $client, $config) {
+    $this->config[get_class($client)] = $config;
+  }
+
+  /**
    * Return defaults for feed configuration.
    */
   public function configDefaults() {
@@ -340,7 +611,8 @@ class FeedsSource extends FeedsConfigura
     foreach ($this->importer->plugin_types as $type) {
       if ($this->importer->$type->hasSourceConfig()) {
         $class = get_class($this->importer->$type);
-        $form[$class] = $this->importer->$type->sourceForm($this->config[$class]);
+        $config = isset($this->config[$class]) ? $this->config[$class] : array();
+        $form[$class] = $this->importer->$type->sourceForm($config);
         $form[$class]['#tree'] = TRUE;
       }
     }
@@ -358,4 +630,83 @@ class FeedsSource extends FeedsConfigura
       }
     }
   }
+
+  /**
+   * Writes to feeds log.
+   */
+  public function log($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE) {
+    feeds_log($this->id, $this->feed_nid, $type, $message, $variables, $severity);
+  }
+
+  /**
+   * Background job helper. Starts a background job using Job Scheduler.
+   *
+   * Execute the first batch chunk of a background job on the current page load,
+   * moves the rest of the job processing to a cron powered background job.
+   *
+   * Executing the first batch chunk is important, otherwise, when a user
+   * submits a source for import or clearing, we will leave her without any
+   * visual indicators of an ongoing job.
+   *
+   * @see FeedsSource::startImport().
+   * @see FeedsSource::startClear().
+   *
+   * @param $method
+   *   Method to execute on importer; one of 'import' or 'clear'.
+   *
+   * @throws Exception $e
+   */
+  protected function startBackgroundJob($method) {
+    if (FEEDS_BATCH_COMPLETE != $this->$method()) {
+      $job = array(
+        'callback' => "feeds_source_{$method}",
+        'type' => $this->id,
+        'id' => $this->feed_nid,
+        'period' => 0,
+        'periodic' => FALSE,
+      );
+      job_scheduler()->set($job);
+    }
+  }
+
+  /**
+   * Batch API helper. Starts a Batch API job.
+   *
+   * @see FeedsSource::startImport().
+   * @see FeedsSource::startClear().
+   * @see feeds_batch()
+   *
+   * @param $title
+   *   Title to show to user when executing batch.
+   * @param $method
+   *   Method to execute on importer; one of 'import' or 'clear'.
+   */
+  protected function startBatchAPIJob($title, $method) {
+    $batch = array(
+      'title' => $title,
+      'operations' => array(
+        array('feeds_batch', array($method, $this->id, $this->feed_nid)),
+      ),
+      'progress_message' => '',
+    );
+    batch_set($batch);
+  }
+  /**
+   * Acquires a lock for this source.
+   *
+   * @throws FeedsLockException
+   *   If a lock for the requested job could not be acquired.
+   */
+  protected function acquireLock() {
+    if (!lock_acquire("feeds_source_{$this->id}_{$this->feed_nid}", 60.0)) {
+      throw new FeedsLockException(t('Cannot acquire lock for source @id / @feed_nid.', array('@id' => $this->id, '@feed_nid' => $this->feed_nid)));
+    }
+  }
+
+  /**
+   * Releases a lock for this source.
+   */
+  protected function releaseLock() {
+    lock_release("feeds_source_{$this->id}_{$this->feed_nid}");
+  }
 }
Index: libraries/http_request.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/libraries/http_request.inc,v
retrieving revision 1.8.2.1
diff -u -p -r1.8.2.1 http_request.inc
--- libraries/http_request.inc	28 Oct 2010 20:44:12 -0000	1.8.2.1
+++ libraries/http_request.inc	3 Feb 2011 13:03:58 -0000
@@ -130,7 +130,7 @@ function http_request_get($url, $usernam
     // https.
     // Validate in PHP, CURLOPT_PROTOCOLS is only supported with cURL 7.19.4
     $uri = parse_url($url);
-    if ($uri['scheme'] != 'http' && $uri['scheme'] != 'https') {
+    if (isset($uri['scheme']) && $uri['scheme'] != 'http' && $uri['scheme'] != 'https') {
       $result->error = 'invalid schema '. $uri['scheme'];
       $result->code = -1003; // This corresponds to drupal_http_request()
     }
Index: mappers/content.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/Attic/content.inc,v
retrieving revision 1.5
diff -u -p -r1.5 content.inc
--- mappers/content.inc	16 May 2010 21:15:53 -0000	1.5
+++ mappers/content.inc	3 Feb 2011 13:03:58 -0000
@@ -7,11 +7,14 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  *
  * @see FeedsNodeProcessor::getMappingTargets().
  */
-function content_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function content_feeds_processor_targets_alter(&$targets, $entity_type, $content_type = NULL) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
   $fields = array();
   if (isset($info['fields']) && count($info['fields'])) {
@@ -37,7 +40,7 @@ function content_feeds_node_processor_ta
  * user has decided to map to and $value contains the value of the feed item
  * element the user has picked as a source.
  */
-function content_feeds_set_target($node, $target, $value) {
+function content_feeds_set_target($source, $node, $target, $value) {
 
   $field = isset($node->$target) ? $node->$target : array();
 
Index: mappers/content_taxonomy.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/Attic/content_taxonomy.inc,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 content_taxonomy.inc
--- mappers/content_taxonomy.inc	28 Oct 2010 20:14:28 -0000	1.1.2.1
+++ mappers/content_taxonomy.inc	3 Feb 2011 13:03:58 -0000
@@ -8,11 +8,14 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  *
  * @see FeedsNodeProcessor::getMappingTargets().
  */
-function content_taxonomy_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function content_taxonomy_feeds_processor_targets_alter(&$targets, $entity_type, $content_type = NULL) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
   $fields = array();
   if (isset($info['fields']) && count($info['fields'])) {
@@ -35,7 +38,7 @@ function content_taxonomy_feeds_node_pro
  * @param $node
  *   Reference to the node object we are working on.
  *
- * @param $vid
+ * @param $field_name
  *   The selected content_taxonomy CCK field.
  *
  * @param $terms
@@ -45,7 +48,7 @@ function content_taxonomy_feeds_node_pro
  * @see taxonomy_terms_parse_string().
  *
  */
-function content_taxonomy_feeds_set_target(&$node, $field_name, $terms) {
+function content_taxonomy_feeds_set_target($source, $node, $field_name, $terms) {
   static $fields = array();
 
   $field = content_fields($field_name, $node->type);
Index: mappers/date.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/date.inc,v
retrieving revision 1.3.2.1
diff -u -p -r1.3.2.1 date.inc
--- mappers/date.inc	28 Oct 2010 20:18:59 -0000	1.3.2.1
+++ mappers/date.inc	3 Feb 2011 13:03:58 -0000
@@ -7,13 +7,16 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  *
  * @see FeedsNodeProcessor::getMappingTargets().
  *
  * @todo Only provides "end date" target if field allows it.
  */
-function date_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function date_feeds_processor_targets_alter(&$targets, $entity_type, $content_type = NULL) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
   if (isset($info['fields']) && count($info['fields'])) {
     foreach ($info['fields'] as $field_name => $field) {
@@ -49,7 +52,7 @@ function date_feeds_node_processor_targe
  *
  * @todo Support array of values for dates.
  */
-function date_feeds_set_target($node, $target, $feed_element) {
+function date_feeds_set_target($source, $node, $target, $feed_element) {
   list($field_name, $sub_field) = split(':', $target);
   if (!($feed_element instanceof FeedsDateTimeElement)) {
     if (is_array($feed_element)) {
Index: mappers/email.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/Attic/email.inc,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 email.inc
--- mappers/email.inc	16 Nov 2010 23:02:18 -0000	1.1.2.1
+++ mappers/email.inc	3 Feb 2011 13:03:58 -0000
@@ -7,11 +7,14 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  *
  * @see FeedsNodeProcessor::getMappingTargets().
  */
-function email_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function email_feeds_processor_targets_alter(&$targets, $entity_type, $content_type = NULL) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
   $fields = array();
   if (isset($info['fields']) && count($info['fields'])) {
@@ -37,7 +40,7 @@ function email_feeds_node_processor_targ
  * user has decided to map to and $value contains the value of the feed item
  * element the user has picked as a source.
  */
-function email_feeds_set_target($node, $target, $value) {
+function email_feeds_set_target($source, $node, $target, $value) {
 
   $field = isset($node->$target) ? $node->$target : array();
 
Index: mappers/emfield.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/emfield.inc,v
retrieving revision 1.1.2.2
diff -u -p -r1.1.2.2 emfield.inc
--- mappers/emfield.inc	22 Sep 2010 00:06:00 -0000	1.1.2.2
+++ mappers/emfield.inc	3 Feb 2011 13:03:58 -0000
@@ -8,11 +8,14 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  *
  * @see FeedsNodeProcessor::getMappingTargets().
  */
-function emfield_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function emfield_feeds_processor_targets_alter(&$targets, $entity_type, $content_type = NULL) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
   $fields = array();
   if (isset($info['fields']) && count($info['fields'])) {
@@ -38,7 +41,7 @@ function emfield_feeds_node_processor_ta
  * user has decided to map to and $value contains the value of the feed item
  * element the user has picked as a source.
  */
-function emfield_feeds_set_target(&$node, $target, $value) {
+function emfield_feeds_set_target($source, $node, $target, $value) {
   $field = isset($node->$target) ? $node->$target : array();
 
   // Handle non-multiple value fields.
@@ -59,4 +62,4 @@ function emfield_feeds_set_target(&$node
   }
 
   $node->$target = $field;
-}
\ No newline at end of file
+}
Index: mappers/filefield.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/Attic/filefield.inc,v
retrieving revision 1.5
diff -u -p -r1.5 filefield.inc
--- mappers/filefield.inc	18 Jul 2010 18:42:31 -0000	1.5
+++ mappers/filefield.inc	3 Feb 2011 13:03:58 -0000
@@ -10,9 +10,12 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter()
+ * Implementation of hook_feeds_processor_targets_alter()
  */
-function filefield_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function filefield_feeds_processor_targets_alter(&$targets, $entity_type, $content_type = NULL) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
   $fields = array();
   if (isset($info['fields']) && count($info['fields'])) {
@@ -42,7 +45,7 @@ function filefield_feeds_node_processor_
  *
  * @todo: should we support $object->url again?
  */
-function filefield_feeds_set_target($node, $field_name, $value) {
+function filefield_feeds_set_target($source, $node, $field_name, $value) {
   // Normalize $value, create an array of FeedsEnclosures of it.
   $enclosures = array();
   if (!is_array($value)) {
Index: mappers/link.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/link.inc,v
retrieving revision 1.4
diff -u -p -r1.4 link.inc
--- mappers/link.inc	14 Sep 2010 15:00:47 -0000	1.4
+++ mappers/link.inc	3 Feb 2011 13:03:58 -0000
@@ -7,9 +7,12 @@
  */
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  */
-function link_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function link_feeds_processor_targets_alter(&$targets, $entity_type, $content_type) {
+  if ($entity_type != 'node') {
+    return;
+  }
   $info = content_types($content_type);
 
   $fields = array();
@@ -49,7 +52,7 @@ function link_feeds_node_processor_targe
  * @param $value
  *   The value to assign to the CCK field.
  */
-function link_feeds_set_target($node, $target, $value) {
+function link_feeds_set_target($source, $node, $target, $value) {
   module_load_include('inc', 'link');
   if (!empty($value)) {
     static $defaults = array();
Index: mappers/locale.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/locale.inc,v
retrieving revision 1.2.2.1
diff -u -p -r1.2.2.1 locale.inc
--- mappers/locale.inc	25 Oct 2010 22:43:03 -0000	1.2.2.1
+++ mappers/locale.inc	3 Feb 2011 13:03:58 -0000
@@ -26,16 +26,19 @@ function locale_feeds_parser_sources_alt
 /**
  * Callback, returns specific locale settings of the parent feed node.
  */
-function locale_feeds_get_source(FeedsImportBatch $batch, $key) {
-  if ($node = node_load($batch->feed_nid)) {
+function locale_feeds_get_source(FeedsSource $source, FeedsParserResult $result, $key) {
+  if ($node = node_load($source->feed_nid)) {
     return $node->language;
   }
 }
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  */
-function locale_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function locale_feeds_processor_targets_alter(&$targets, $entity_type, $content_type) {
+  if ($entity_type != 'node') {
+    return;
+  }
   if (variable_get('language_content_type_'. $content_type, FALSE)) {
     $targets['language'] = array(
       'name' => t('Language'),
@@ -48,6 +51,6 @@ function locale_feeds_node_processor_tar
 /**
  * Callback for mapping.
  */
-function locale_feeds_set_target($node, $key, $language) {
+function locale_feeds_set_target($source, $node, $key, $language) {
   $node->language = $language;
 }
Index: mappers/og.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/og.inc,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 og.inc
--- mappers/og.inc	25 Oct 2010 22:43:03 -0000	1.1.2.1
+++ mappers/og.inc	3 Feb 2011 13:03:58 -0000
@@ -22,8 +22,8 @@ function og_feeds_parser_sources_alter(&
 /**
  * Callback, returns OG of feed node.
  */
-function og_feeds_get_source(FeedsImportBatch $batch, $key) {
-  if ($node = node_load($batch->feed_nid)) {
+function og_feeds_get_source(FeedsSource $source, FeedsParserResult $result, $key) {
+  if ($node = node_load($source->feed_nid)) {
     if (in_array($node->type, og_get_types('group'))) {
       return array(
         $node->nid => $node->nid,
@@ -36,9 +36,12 @@ function og_feeds_get_source(FeedsImport
 }
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  */
-function og_feeds_node_processor_targets_alter(&$targets, $content_type) {
+function og_feeds_processor_targets_alter(&$targets, $entity_type, $content_type) {
+  if ($entity_type != 'node') {
+    return;
+  }
   if (in_array($content_type, og_get_types('group_post'))) {
     $targets['og_groups'] = array(
       'name' => t('Organic group(s)'),
@@ -51,6 +54,6 @@ function og_feeds_node_processor_targets
 /**
  * Callback for mapping.
  */
-function og_feeds_set_target($node, $key, $groups) {
+function og_feeds_set_target($source, $node, $key, $groups) {
   $node->og_groups = $groups;
 }
Index: mappers/profile.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/profile.inc,v
retrieving revision 1.1
diff -u -p -r1.1 profile.inc
--- mappers/profile.inc	19 Jun 2010 15:32:07 -0000	1.1
+++ mappers/profile.inc	3 Feb 2011 13:03:59 -0000
@@ -7,9 +7,12 @@
  */
 
 /**
- * Implementation of feeds_user_processor_target_alter().
+ * Implementation of feeds_processor_target_alter().
  */
-function profile_feeds_user_processor_targets_alter(&$targets) {
+function profile_feeds_processor_targets_alter(&$targets, $entity_type, $content_type) {
+  if ($entity_type != 'user') {
+    return;
+  }
   if (module_exists('profile')) {
     $categories = profile_categories();
 
@@ -29,7 +32,7 @@ function profile_feeds_user_processor_ta
 /**
  * Set the user profile target after import.
  */
-function profile_feeds_set_target($account, $target, $value) {
+function profile_feeds_set_target($source, $account, $target, $value) {
   $account->{$target} = $value;
   return $account;
 }
Index: mappers/taxonomy.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/mappers/taxonomy.inc,v
retrieving revision 1.11.2.2
diff -u -p -r1.11.2.2 taxonomy.inc
--- mappers/taxonomy.inc	28 Oct 2010 20:49:38 -0000	1.11.2.2
+++ mappers/taxonomy.inc	3 Feb 2011 13:03:59 -0000
@@ -24,8 +24,8 @@ function taxonomy_feeds_parser_sources_a
 /**
  * Callback, returns taxonomy from feed node.
  */
-function taxonomy_feeds_get_source(FeedsImportBatch $batch, $key) {
-  if ($node = node_load($batch->feed_nid)) {
+function taxonomy_feeds_get_source(FeedsSource $source, FeedsParserResult $result, $key) {
+  if ($node = node_load($source->feed_nid)) {
     $terms = taxonomy_node_get_terms($node);
     $vocabulary = taxonomy_get_vocabulary(str_replace('parent:taxonomy:', '', $key));
     $result = array();
@@ -39,30 +39,35 @@ function taxonomy_feeds_get_source(Feeds
 }
 
 /**
- * Implementation of hook_feeds_node_processor_targets_alter().
+ * Implementation of hook_feeds_processor_targets_alter().
  *
  * @see FeedsNodeProcessor::getMappingTargets().
  */
-function taxonomy_feeds_node_processor_targets_alter(&$targets, $content_type) {
-  foreach (taxonomy_get_vocabularies($content_type) as $vocabulary) {
-    $description = t('The @name vocabulary of the node. If this is a "Tags" vocabulary, any new terms will be created on import. Otherwise only existing terms will be used. If this is not a "Tags" vocabulary and not a "Multiple select" vocabulary, only the first available term will be created. See !settings.', array('@name' => $vocabulary->name, '!settings' => l(t('vocabulary settings'), 'admin/content/taxonomy/edit/vocabulary/'. $vocabulary->vid, array('query' => 'destination='. $_GET['q']))));
-
-    $targets['taxonomy:'. taxonomy_vocabulary_id($vocabulary)] = array(
-      'name' => "Taxonomy: ". $vocabulary->name,
-      'callback' => 'taxonomy_feeds_set_target',
-      'description' => $description,
-      'real_target' => 'taxonomy',
-    );
+function taxonomy_feeds_processor_targets_alter(&$targets, $entity_type, $content_type) {
+  if ($entity_type == 'node') {
+    foreach (taxonomy_get_vocabularies($content_type) as $vocabulary) {
+      $description = t('The @name vocabulary of the node. If this is a "Tags" vocabulary, any new terms will be created on import. Otherwise only existing terms will be used. If this is not a "Tags" vocabulary and not a "Multiple select" vocabulary, only the first available term will be created. See !settings.', array('@name' => $vocabulary->name, '!settings' => l(t('vocabulary settings'), 'admin/content/taxonomy/edit/vocabulary/'. $vocabulary->vid, array('query' => 'destination='. $_GET['q']))));
+
+      $targets['taxonomy:'. taxonomy_vocabulary_id($vocabulary)] = array(
+        'name' => "Taxonomy: ". $vocabulary->name,
+        'callback' => 'taxonomy_feeds_set_target',
+        'description' => $description,
+        'real_target' => 'taxonomy',
+      );
+    }
   }
 }
 
 /**
  * Callback for mapping. Here is where the actual mapping happens.
  *
+ * @param
+ *  The feeds source
+ *
  * @param $node
- *   Reference to the node object we are working on.
+ *   Reference to the entity object we are working on. i.e. node
  *
- * @param $key
+ * @param $target
  *   A key as added to the $targets array by
  *   taxonomy_feeds_node_processor_targets_alter().
  *
@@ -72,8 +77,7 @@ function taxonomy_feeds_node_processor_t
  * Add the given terms to the node object so the taxonomy module can add them
  * on node_save().
  */
-function taxonomy_feeds_set_target(&$node, $key, $terms) {
-
+function taxonomy_feeds_set_target($source, $node, $key, $terms) {
   // Return if there are no terms.
   if (empty($terms)) {
     return;
@@ -82,12 +86,20 @@ function taxonomy_feeds_set_target(&$nod
   // Load target vocabulary to check, if it has the "tags" flag.
   $vocabulary = taxonomy_get_vocabulary(str_replace('taxonomy:', '', $key));
 
+  if (!isset($node->taxonomy)) {
+    $node->taxonomy = array();
+  }
+
   // Cast a given single string to an array so we can use it.
   if (!is_array($terms)) {
     $terms = array($terms);
   }
 
   if ($vocabulary->tags) {
+    if (!isset($node->taxonomy['tags'])) {
+      $node->taxonomy['tags'] = array();
+      $node->taxonomy['tags'][$vocabulary->vid] = '';
+    }
     foreach ($terms as $k => $v) {
       // Make sure there aren't any terms with a comma (=tag delimiter) in it.
       $terms[$k] = preg_replace('/\s*,\s*/', ' ', $v);
Index: plugins/FeedsCSVParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsCSVParser.inc,v
retrieving revision 1.16.2.3
diff -u -p -r1.16.2.3 FeedsCSVParser.inc
--- plugins/FeedsCSVParser.inc	29 Oct 2010 18:44:35 -0000	1.16.2.3
+++ plugins/FeedsCSVParser.inc	3 Feb 2011 13:03:59 -0000
@@ -9,16 +9,17 @@ class FeedsCSVParser extends FeedsParser
   /**
    * Implementation of FeedsParser::parse().
    */
-  public function parse(FeedsImportBatch $batch, FeedsSource $source) {
+  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
+    $source_config = $source->getConfigFor($this);
+    $state = $source->state(FEEDS_PARSE);
 
     // Load and configure parser.
     feeds_include_library('ParserCSV.inc', 'ParserCSV');
-    $iterator = new ParserCSVIterator(realpath($batch->getFilePath()));
-    $source_config = $source->getConfigFor($this);
     $parser = new ParserCSV();
     $delimiter = $source_config['delimiter'] == 'TAB' ? "\t" : $source_config['delimiter'];
     $parser->setDelimiter($delimiter);
 
+    $iterator = new ParserCSVIterator($fetcher_result->getFilePath());
     if (empty($source_config['no_headers'])) {
       // Get first line and use it for column names, convert them to lower case.
       $header = $this->parseHeader($parser, $iterator);
@@ -28,8 +29,19 @@ class FeedsCSVParser extends FeedsParser
       $parser->setColumnNames($header);
     }
 
-    // Populate batch.
-    $batch->items = $this->parseItems($parser, $iterator);
+    // Determine section to parse, parse.
+    $start = $state->pointer ? $state->pointer : $parser->lastLinePos();
+    $limit = $source->importer->getLimit();
+    $rows = $this->parseItems($parser, $iterator, $start, $limit);
+
+    // Report progress.
+    $state->total = filesize($fetcher_result->getFilePath());
+    $state->pointer = $parser->lastLinePos();
+    $progress = $parser->lastLinePos() ? $parser->lastLinePos() : $state->total;
+    $state->progress($state->total, $progress);
+
+    // Create a result object and return it.
+    return new FeedsParserResult($rows, $source->feed_nid);
   }
 
   /**
@@ -63,10 +75,9 @@ class FeedsCSVParser extends FeedsParser
    * @return
    *   An array of rows of the CSV keyed by the column names previously set
    */
-  protected function parseItems(ParserCSV $parser, ParserCSVIterator $iterator) {
-    // Set line limit to 0 and start byte to last position and parse rest.
-    $parser->setLineLimit(0);
-    $parser->setStartByte($parser->lastLinePos());
+  protected function parseItems(ParserCSV $parser, ParserCSVIterator $iterator, $start = 0, $limit = 0) {
+    $parser->setLineLimit($limit);
+    $parser->setStartByte($start);
     $rows = $parser->parse($iterator);
     return $rows;
   }
@@ -81,8 +92,8 @@ class FeedsCSVParser extends FeedsParser
   /**
    * Override parent::getSourceElement() to use only lower keys.
    */
-  public function getSourceElement(FeedsImportBatch $batch, $element_key) {
-    return parent::getSourceElement($batch, drupal_strtolower($element_key));
+  public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
+    return parent::getSourceElement($source, $result, drupal_strtolower($element_key));
   }
 
   /**
@@ -113,12 +124,11 @@ class FeedsCSVParser extends FeedsParser
       }
     }
 
-    $items = array(
-      t('Import !csv_files with one or more of these columns: !columns.', array('!csv_files' => l(t('CSV files'), 'http://en.wikipedia.org/wiki/Comma-separated_values'), '!columns' => implode(', ', $sources))),
-      format_plural(count($uniques), t('Column <strong>!column</strong> is mandatory and considered unique: only one item per !column value will be created.', array('!column' => implode(', ', $uniques))), t('Columns <strong>!columns</strong> are mandatory and values in these columns are considered unique: only one entry per value in one of these column will be created.', array('!columns' => implode(', ', $uniques)))),
-    );
-    $form['help']['#value'] = '<div class="help">'. theme('item_list', $items) .'</div>';
-
+    $output = t('Import !csv_files with one or more of these columns: !columns.',array('!csv_files' => l(t('CSV files'), 'http://en.wikipedia.org/wiki/Comma-separated_values'), '!columns' => implode(', ', $sources)));
+    $items = array();
+    $items[] = format_plural(count($uniques), t('Column <strong>!column</strong> is mandatory and considered unique: only one item per !column value will be created.', array('!column' => implode(', ', $uniques))), t('Columns <strong>!columns</strong> are mandatory and values in these columns are considered unique: only one entry per value in one of these column will be created.', array('!columns' => implode(', ', $uniques))));
+    $items[] = l(t('Download a template'), 'import/' . $this->id . '/template');
+    $form['help']['#value'] = '<div class="help"><p>' . $output . '</p>' . theme('item_list', $items) . '</div>';
     $form['delimiter'] = array(
       '#type' => 'select',
       '#title' => t('Delimiter'),
Index: plugins/FeedsFetcher.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsFetcher.inc,v
retrieving revision 1.7
diff -u -p -r1.7 FeedsFetcher.inc
--- plugins/FeedsFetcher.inc	15 Sep 2010 19:40:57 -0000	1.7
+++ plugins/FeedsFetcher.inc	3 Feb 2011 13:03:59 -0000
@@ -2,9 +2,110 @@
 // $Id: FeedsFetcher.inc,v 1.7 2010/09/15 19:40:57 alexb Exp $
 
 /**
+ * Base class for all fetcher results.
+ */
+class FeedsFetcherResult extends FeedsResult {
+  protected $raw;
+  protected $file_path;
+
+  /**
+   * Constructor.
+   */
+  public function __construct($raw) {
+    $this->raw = $raw;
+  }
+
+  /**
+   * @return
+   *   The raw content from the source as a string.
+   *
+   * @throws Exception
+   *   Extending classes MAY throw an exception if a problem occurred.
+   */
+  public function getRaw() {
+    return $this->sanitizeRaw($this->raw);
+  }
+
+  /**
+   * Get a path to a temporary file containing the resource provided by the
+   * fetcher.
+   *
+   * File will be deleted after DRUPAL_MAXIMUM_TEMP_FILE_AGE.
+   *
+   * @return
+   *   A path to a file containing the raw content as a source.
+   *
+   * @throws Exception
+   *   If an unexpected problem occurred.
+   */
+  public function getFilePath() {
+    if (!isset($this->file_path)) {
+      $destination = 'public://feeds';
+      if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+        throw new Exception(t('Feeds directory either cannot be created or is not writable.'));
+      }
+      $this->file_path = FALSE;
+      if ($file = file_save_data($this->getRaw(), $destination . '/'. get_class($this) . FEEDS_REQUEST_TIME)) {
+        $file->status = 0;
+        file_save($file);
+        $this->file_path = $file->uri;
+      }
+      else {
+        throw new Exception(t('Cannot write content to %dest', array('%dest' => $destination)));
+      }
+    }
+    return $this->sanitizeFile($this->file_path);
+  }
+
+  /**
+   * Sanitize the raw content string. Currently supported sanitizations:
+   *
+   * - Remove BOM header from UTF-8 files.
+   *
+   * @param string $raw
+   *   The raw content string to be sanitized.
+   * @return
+   *   The sanitized content as a string.
+   */
+  public function sanitizeRaw($raw) {
+    if (substr($raw, 0,3) == pack('CCC',0xef,0xbb,0xbf)) {
+      $raw = substr($raw, 3);
+    }
+    return $raw;
+  }
+
+  /**
+   * Sanitize the file in place. Currently supported sanitizations:
+   *
+   * - Remove BOM header from UTF-8 files.
+   *
+   * @param string $filepath
+   *   The file path of the file to be sanitized.
+   * @return
+   *   The file path of the sanitized file.
+   */
+  public function sanitizeFile($filepath) {
+    $handle = fopen($filepath, 'r');
+    $line = fgets($handle);
+    fclose($handle);
+    // If BOM header is present, read entire contents of file and overwrite
+    // the file with corrected contents.
+    if (substr($line, 0,3) == pack('CCC',0xef,0xbb,0xbf)) {
+      $contents = file_get_contents($filepath);
+      $contents = substr($contents, 3);
+      $status = file_put_contents($filepath, $contents);
+      if ($status === FALSE) {
+        throw new Exception(t('File @filepath is not writeable.', array('@filepath' => $filepath)));
+      }
+    }
+    return $filepath;
+  }
+}
+
+/**
  * Abstract class, defines shared functionality between fetchers.
  *
- * Implements FeedsSourceInfoInterface to expose source forms to Feeds.
+ * Implementation of FeedsSourceInfoInterface to expose source forms to Feeds.
  */
 abstract class FeedsFetcher extends FeedsPlugin {
 
@@ -15,6 +116,9 @@ abstract class FeedsFetcher extends Feed
    *
    * @param $source
    *   Source value as entered by user through sourceForm().
+   *
+   * @return
+   *   A FeedsFetcherResult object.
    */
   public abstract function fetch(FeedsSource $source);
 
Index: plugins/FeedsFileFetcher.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsFileFetcher.inc,v
retrieving revision 1.15.2.1
diff -u -p -r1.15.2.1 FeedsFileFetcher.inc
--- plugins/FeedsFileFetcher.inc	28 Oct 2010 20:06:18 -0000	1.15.2.1
+++ plugins/FeedsFileFetcher.inc	3 Feb 2011 13:03:59 -0000
@@ -10,32 +10,30 @@
  * Definition of the import batch object created on the fetching stage by
  * FeedsFileFetcher.
  */
-class FeedsFileBatch extends FeedsImportBatch {
-  protected $file_path;
-
+class FeedsFileFetcherResult extends FeedsFetcherResult {
   /**
    * Constructor.
    */
-  public function __construct($file_path, $feed_nid = 0) {
+  public function __construct($file_path) {
+    parent::__construct('');
     $this->file_path = $file_path;
-    parent::__construct('', $feed_nid);
   }
 
   /**
-   * Implementation of FeedsImportBatch::getRaw();
+   * Overrides parent::getRaw();
    */
   public function getRaw() {
-    return file_get_contents(realpath($this->file_path));
+    return $this->sanitizeRaw(file_get_contents($this->file_path));
   }
 
   /**
-   * Implementation of FeedsImportBatch::getFilePath().
+   * Overrides parent::getFilePath().
    */
   public function getFilePath() {
     if (!file_exists($this->file_path)) {
       throw new Exception(t('File @filepath is not accessible.', array('@filepath' => $this->file_path)));
     }
-    return $this->file_path;
+    return $this->sanitizeFile($this->file_path);
   }
 }
 
@@ -49,39 +47,82 @@ class FeedsFileFetcher extends FeedsFetc
    */
   public function fetch(FeedsSource $source) {
     $source_config = $source->getConfigFor($this);
-    return new FeedsFileBatch($source_config['source'], $source->feed_nid);
+
+    // Just return a file fetcher result if this is a file.
+    if (is_file($source_config['source'])) {
+      return new FeedsFileFetcherResult($source_config['source']);
+    }
+
+    // Batch if this is a directory.
+    $state = $source->state(FEEDS_FETCH);
+    $files = array();
+    if (!isset($state->files)) {
+      $state->files = $this->listFiles($source_config['source']);
+      $state->total = count($state->files);
+    }
+    if (count($state->files)) {
+      $file = array_shift($state->files);
+      $state->progress($state->total, $state->total - count($state->files));
+      return new FeedsFileFetcherResult($file);
+    }
+
+    throw new Exception(t('Resource is not a file or it is an empty directory: %source', array('%source' => $source_config['source'])));
+  }
+
+  /**
+   * Return an array of files in a directory.
+   *
+   * @param $dir
+   *   A stream wreapper URI that is a directory.
+   *
+   * @return
+   *   An array of stream wrapper URIs pointing to files. The array is empty
+   *   if no files could be found. Never contains directories.
+   */
+  protected function listFiles($dir) {
+    //$dir = file_stream_wrapper_uri_normalize($dir);
+    $files = array();
+    if ($items = @scandir($dir)) {
+      foreach ($items as $item) {
+        if (is_file("$dir/$item") && strpos($item, '.') !== 0) {
+          $files[] = "$dir/$item";
+        }
+      }
+    }
+    return $files;
   }
 
   /**
    * Source form.
    */
   public function sourceForm($source_config) {
-    $form = $info = array();
-    if (!empty($source_config['source']) && file_exists($source_config['source'])) {
-      $info = array(
-        'path' => $source_config['source'],
-        'size' => filesize(realpath($source_config['source'])),
+    $form = array();
+    $form['fid'] = array(
+      '#type' => 'value',
+      '#value' => empty($source_config['fid']) ? 0 : $source_config['fid'],
+    );
+    if (empty($this->config['direct'])) {
+      $form['source'] = array(
+        '#type' => 'value',
+        '#value' => empty($source_config['source']) ? '' : $source_config['source'],
+      );
+      $form['upload'] = array(
+        '#type' => 'file',
+        '#title' => empty($this->config['direct']) ? t('File') : NULL,
+        '#description' => empty($source_config['source']) ? t('Select a file from your local system.') : t('Select a different file from your local system.'),
+        '#theme' => 'feeds_upload',
+        '#file_info' => empty($source_config['fid']) ? NULL : $this->loadFile($source_config['fid']),
+        '#size' => 10,
+      );
+    }
+    else {
+      $form['source'] = array(
+        '#type' => 'textfield',
+        '#title' => t('File'),
+        '#description' => t('Specify a path to a file or a directory. Path must start with @scheme://', array('@scheme' => file_default_scheme())),
+        '#default_value' => empty($source_config['source']) ? '' : $source_config['source'],
       );
-      if (module_exists('mimedetect')) {
-        $file = new stdClass;
-        $file->filepath = realpath($source_config['source']);
-        $info['mime'] = mimedetect_mime($file);
-      }
     }
-    $form['source'] = array(
-      '#type' => empty($this->config['direct']) ? 'value' : 'textfield',
-      '#title' => t('File'),
-      '#description' => t('Specify a file in the site\'s file system path or upload a file below.'),
-      '#default_value' => empty($source_config['source']) ? '' : $source_config['source'],
-    );
-    $form['upload'] = array(
-      '#type' => 'file',
-      '#title' => empty($this->config['direct']) ? t('File') : NULL,
-      '#description' => empty($source_config['source']) ? t('Select the file to be imported from your local system.') : t('Select a different file to be imported from your local system.'),
-      '#theme' => 'feeds_upload',
-      '#file_info' => $info,
-      '#size' => 10,
-    );
     return $form;
   }
 
@@ -94,26 +135,59 @@ class FeedsFileFetcher extends FeedsFetc
 
     // If there is a file uploaded, save it, otherwise validate input on
     // file.
-    if ($file = file_save_upload('feeds', array(), $feed_dir)) {
-      file_set_status($file, FILE_STATUS_PERMANENT);
+    // @todo: Track usage of file, remove file when removing source.
+    if ($file = file_save_upload('feeds', array('file_validate_extensions' => array(0 => $this->config['allowed_extensions'])), $feed_dir)) {
       $values['source'] = $file->filepath;
+      $values['file'] = $file;
     }
     elseif (empty($values['source'])) {
       form_set_error('feeds][source', t('Upload a file first.'));
     }
     // If a file has not been uploaded and $values['source'] is not empty, make
     // sure that this file is within Drupal's files directory as otherwise
-    // potentially any file that the web server has access could be exposed.
+    // potentially any file that the web server has access to could be exposed.
     elseif (!file_check_location($values['source'], file_directory_path())) {
       form_set_error('feeds][source', t('File needs to point to a file in your Drupal file system path.'));
     }
   }
 
   /**
+   * Override parent::sourceSave().
+   */
+  public function sourceSave(FeedsSource $source) {
+    $source_config = $source->getConfigFor($this);
+
+    // If a new file is present, delete the old one and replace it with the new
+    // one.
+    if (isset($source_config['file'])) {
+      $file = $source_config['file'];
+      if (isset($source_config['fid'])) {
+        $this->deleteFile($source_config['fid'], $source->feed_nid);
+      }
+      file_set_status($file, FILE_STATUS_PERMANENT);
+
+      $source_config['fid'] = $file->fid;
+      unset($source_config['file']);
+      $source->setConfigFor($this, $source_config);
+    }
+  }
+
+  /**
+   * Override parent::sourceDelete().
+   */
+  public function sourceDelete(FeedsSource $source) {
+    $source_config = $source->getConfigFor($this);
+    if (isset($source_config['fid'])) {
+      $this->deleteFile($source_config['fid'], $source->feed_nid);
+    }
+  }
+
+  /**
    * Override parent::configDefaults().
    */
   public function configDefaults() {
     return array(
+      'allowed_extensions' => 'txt csv tsv xml opml rss2',
       'direct' => FALSE,
     );
   }
@@ -123,12 +197,34 @@ class FeedsFileFetcher extends FeedsFetc
    */
   public function configForm(&$form_state) {
     $form = array();
+    $form['allowed_extensions'] = array(
+      '#type' =>'textfield',
+      '#title' => t('Allowed file extensions'),
+      '#description' => t('Allowed file extensions for upload.'),
+      '#default_value' => $this->config['allowed_extensions'],
+    );
     $form['direct'] = array(
       '#type' =>'checkbox',
-      '#title' => t('Supply path to file directly'),
-      '#description' => t('For experts. If checked users can specify a path to a file when importing rather than uploading a file. This is useful when files to be imported are already present on server.'),
+      '#title' => t('Supply path to file or directory directly'),
+      '#description' => t('For experts. Lets users specify a path to a file <em>or a directory of files</em> directly,
+        instead of a file upload through the browser. This is useful when the files that need to be imported
+        are already on the server.'),
       '#default_value' => $this->config['direct'],
     );
     return $form;
   }
+
+  /**
+   * Helper. Deletes a file.
+   */
+  protected function deleteFile($fid, $feed_nid) {
+    if ($filepath = db_result(db_query("SELECT filepath FROM {files} WHERE fid = %d", $fid))) {
+      file_delete($filepath);
+      db_query("DELETE FROM {files} WHERE fid = %d", $fid);
+    }
+  }
+
+  protected function loadFile($fid) {
+    return db_fetch_array(db_query("SELECT * FROM {files} WHERE fid = %d", $fid));
+  }
 }
Index: plugins/FeedsHTTPFetcher.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsHTTPFetcher.inc,v
retrieving revision 1.25
diff -u -p -r1.25 FeedsHTTPFetcher.inc
--- plugins/FeedsHTTPFetcher.inc	15 Sep 2010 19:40:57 -0000	1.25
+++ plugins/FeedsHTTPFetcher.inc	3 Feb 2011 13:04:00 -0000
@@ -9,23 +9,22 @@
 feeds_include_library('PuSHSubscriber.inc', 'PuSHSubscriber');
 
 /**
- * Definition of the import batch object created on the fetching stage by
- * FeedsHTTPFetcher.
+ * Result of FeedsHTTPFetcher::fetch().
  */
-class FeedsHTTPBatch extends FeedsImportBatch {
+class FeedsHTTPFetcherResult extends FeedsFetcherResult {
   protected $url;
   protected $file_path;
 
   /**
    * Constructor.
    */
-  public function __construct($url = NULL, $feed_nid) {
+  public function __construct($url = NULL) {
     $this->url = $url;
-    parent::__construct('', $feed_nid);
+    parent::__construct('');
   }
 
   /**
-   * Implementation of FeedsImportBatch::getRaw();
+   * Overrides FeedsFetcherResult::getRaw();
    */
   public function getRaw() {
     feeds_include_library('http_request.inc', 'http_request');
@@ -33,7 +32,7 @@ class FeedsHTTPBatch extends FeedsImport
     if (!in_array($result->code, array(200, 201, 202, 203, 204, 205, 206))) {
       throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->url, '!code' => $result->code)));
     }
-    return $result->data;
+    return $this->sanitizeRaw($result->data);
   }
 }
 
@@ -48,9 +47,9 @@ class FeedsHTTPFetcher extends FeedsFetc
   public function fetch(FeedsSource $source) {
     $source_config = $source->getConfigFor($this);
     if ($this->config['use_pubsubhubbub'] && ($raw = $this->subscriber($source->feed_nid)->receive())) {
-      return new FeedsImportBatch($raw, $source->feed_nid);
+      return new FeedsFetcherResult($raw);
     }
-    return new FeedsHTTPBatch($source_config['source'], $source->feed_nid);
+    return new FeedsHTTPFetcherResult($source_config['source']);
   }
 
   /**
@@ -122,7 +121,7 @@ class FeedsHTTPFetcher extends FeedsFetc
       '#title' => t('Designated hub'),
       '#description' => t('Enter the URL of a designated PubSubHubbub hub (e. g. superfeedr.com). If given, this hub will be used instead of the hub specified in the actual feed.'),
       '#default_value' => $this->config['designated_hub'],
-      '#process' => array('ctools_dependent_process'),
+//      '#process' => array('ctools_dependent_process'),
       '#dependency' => array(
         'edit-use-pubsubhubbub' => array(1),
       ),
@@ -226,7 +225,9 @@ class PuSHSubscription implements PuSHSu
    * Load a subscription.
    */
   public static function load($domain, $subscriber_id) {
-    if ($v = db_fetch_array(db_query("SELECT * FROM {feeds_push_subscriptions} WHERE domain = '%s' AND subscriber_id = %d", $domain, $subscriber_id))) {
+    if ($v = db_fetch_array(db_query(
+      "SELECT * FROM {feeds_push_subscriptions} WHERE domain = '%s'
+      AND subscriber_id = '%s'", $domain, $subscriber_id))) {
       $v['post_fields'] = unserialize($v['post_fields']);
       return new PuSHSubscription($v['domain'], $v['subscriber_id'], $v['hub'], $v['topic'], $v['secret'], $v['status'], $v['post_fields'], $v['timestamp']);
     }
@@ -258,7 +259,7 @@ class PuSHSubscription implements PuSHSu
    * Delete a subscription.
    */
   public function delete() {
-    db_query("DELETE FROM {feeds_push_subscriptions} WHERE domain = '%s' AND subscriber_id = %d", $this->domain, $this->subscriber_id);
+    db_query("DELETE FROM {feeds_push_subscriptions} WHERE domain = '%s' AND subscriber_id = '%s'", $this->domain, $this->subscriber_id);
   }
 }
 
Index: plugins/FeedsNodeProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsNodeProcessor.inc,v
retrieving revision 1.51.2.2
diff -u -p -r1.51.2.2 FeedsNodeProcessor.inc
--- plugins/FeedsNodeProcessor.inc	25 Oct 2010 22:43:03 -0000	1.51.2.2
+++ plugins/FeedsNodeProcessor.inc	3 Feb 2011 13:04:01 -0000
@@ -6,112 +6,101 @@
  * Class definition of FeedsNodeProcessor.
  */
 
-// Create or delete FEEDS_NODE_BATCH_SIZE at a time.
-define('FEEDS_NODE_BATCH_SIZE', 50);
-
-// Deprecated. Use FEEDS_SKIPE_EXISTING, FEEDS_REPLACE_EXISTNG,
-// FEEDS_UPDATE_EXISTING instead.
-define('FEEDS_NODE_SKIP_EXISTING', 0);
-define('FEEDS_NODE_REPLACE_EXISTING', 1);
-define('FEEDS_NODE_UPDATE_EXISTING', 2);
-
 /**
  * Creates nodes from feed items.
  */
 class FeedsNodeProcessor extends FeedsProcessor {
+  /**
+   * Define entity type.
+   */
+  public function entityType() {
+    return 'node';
+  }
 
   /**
-   * Implementation of FeedsProcessor::process().
+   * Implementation of parent::entityInfo().
    */
-  public function process(FeedsImportBatch $batch, FeedsSource $source) {
+  protected function entityInfo() {
+    //$info = parent::entityInfo();
+    $info = array();
+    $info['label'] = t('Node');
+    $info['label plural'] = t('Nodes');
+    $info['base table'] = 'node';
+    $info['entity keys']['id'] = 'nid';
+    return $info;
+  }
 
-    // Keep track of processed items in this pass, set total number of items.
-    $processed = 0;
-    if (!$batch->getTotal(FEEDS_PROCESSING)) {
-      $batch->setTotal(FEEDS_PROCESSING, count($batch->items));
+  /**
+   * Creates a new node in memory and returns it.
+   */
+  protected function newEntity(FeedsSource $source) {
+    $node = new stdClass();
+    $node->type = $this->config['content_type'];
+    $node->changed = FEEDS_REQUEST_TIME;
+    $node->created = FEEDS_REQUEST_TIME;
+    static $included;
+    if (!$included) {
+      module_load_include('inc', 'node', 'node.pages');
+      $included = TRUE;
     }
+    node_object_prepare($node);
+    // Populate properties that are set by node_object_prepare().
+    $node->log = 'Created by FeedsNodeProcessor';
+    $node->uid = $this->config['author'];
+    return $node;
+  }
 
-    while ($item = $batch->shiftItem()) {
-
-      // Create/update if item does not exist or update existing is enabled.
-      if (!($nid = $this->existingItemId($batch, $source)) || ($this->config['update_existing'] != FEEDS_SKIP_EXISTING)) {
-        // Only proceed if item has actually changed.
-        $hash = $this->hash($item);
-        if (!empty($nid) && $hash == $this->getHash($nid)) {
-          continue;
-        }
-
-        $node = $this->buildNode($nid, $source->feed_nid);
-        $node->feeds_node_item->hash = $hash;
-
-        // Map and save node. If errors occur don't stop but report them.
-        try {
-          $this->map($batch, $node);
-          node_save($node);
-          if (!empty($nid)) {
-            $batch->updated++;
-          }
-          else {
-            $batch->created++;
-          }
-        }
-        catch (Exception $e) {
-          drupal_set_message($e->getMessage(), 'warning');
-          watchdog('feeds', $e->getMessage(), array(), WATCHDOG_WARNING);
-        }
-      }
-
-      $processed++;
-      if ($processed >= variable_get('feeds_node_batch_size', FEEDS_NODE_BATCH_SIZE)) {
-        $batch->setProgress(FEEDS_PROCESSING, $batch->created + $batch->updated);
-        return;
-      }
+  /**
+   * Loads an existing node.
+   *
+   * If the update existing method is not FEEDS_UPDATE_EXISTING, only the node
+   * table will be loaded, foregoing the node_load API for better performance.
+   */
+  protected function entityLoad(FeedsSource $source, $nid) {
+    if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
+      $node = node_load($nid, NULL, TRUE);
     }
-
-    // Set messages.
-    if ($batch->created) {
-      drupal_set_message(format_plural($batch->created, 'Created @number @type node.', 'Created @number @type nodes.', array('@number' => $batch->created, '@type' => node_get_types('name', $this->config['content_type']))));
+    else {
+      // We're replacing the existing node. Only save the absolutely necessary.
+      $node = db_fetch_object(db_query("SELECT created, nid, vid, type FROM {node} WHERE nid = %d", $nid));
+      $node->uid = $this->config['author'];
+    }
+    static $included;
+    if (!$included) {
+      module_load_include('inc', 'node', 'node.pages');
+      $included = TRUE;
     }
-    elseif ($batch->updated) {
-      drupal_set_message(format_plural($batch->updated, 'Updated @number @type node.', 'Updated @number @type nodes.', array('@number' => $batch->updated, '@type' => node_get_types('name', $this->config['content_type']))));
+    node_object_prepare($node);
+    // Populate properties that are set by node_object_prepare().
+    if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
+      $node->log = 'Updated by FeedsNodeProcessor';
     }
     else {
-      drupal_set_message(t('There is no new content.'));
+      $node->log = 'Replaced by FeedsNodeProcessor';
     }
-    $batch->setProgress(FEEDS_PROCESSING, FEEDS_BATCH_COMPLETE);
+    return $node;
   }
 
   /**
-   * Implementation of FeedsProcessor::clear().
+   * Save a node.
    */
-  public function clear(FeedsBatch $batch, FeedsSource $source) {
-    if (!$batch->getTotal(FEEDS_CLEARING)) {
-      $total = db_result(db_query("SELECT COUNT(nid) FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid));
-      $batch->setTotal(FEEDS_CLEARING, $total);
-    }
-    $result = db_query_range("SELECT nid FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid, 0, variable_get('feeds_node_batch_size', FEEDS_NODE_BATCH_SIZE));
-    while ($node = db_fetch_object($result)) {
-      _feeds_node_delete($node->nid);
-      $batch->deleted++;
-    }
-    if (db_result(db_query_range("SELECT nid FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid, 0, 1))) {
-      $batch->setProgress(FEEDS_CLEARING, $batch->deleted);
-      return;
-    }
+  public function entitySave($entity) {
+    node_save($entity);
+  }
 
-    // Set message.
-    drupal_get_messages('status');
-    if ($batch->deleted) {
-      drupal_set_message(format_plural($batch->deleted, 'Deleted @number node.', 'Deleted @number nodes.', array('@number' => $batch->deleted)));
-    }
-    else {
-      drupal_set_message(t('There is no content to be deleted.'));
+  /**
+   * Delete a series of nodes.
+   */
+  protected function entityDeleteMultiple($nids) {
+    foreach ($nids as $nid) {
+      node_delete($nid);
     }
-    $batch->setProgress(FEEDS_CLEARING, FEEDS_BATCH_COMPLETE);
   }
 
   /**
    * Implement expire().
+   *
+   * @todo: move to processor stage?
    */
   public function expire($time = NULL) {
     if ($time === NULL) {
@@ -120,11 +109,22 @@ class FeedsNodeProcessor extends FeedsPr
     if ($time == FEEDS_EXPIRE_NEVER) {
       return;
     }
-    $result = db_query_range("SELECT n.nid FROM {node} n JOIN {feeds_node_item} fni ON n.nid = fni.nid WHERE fni.id = '%s' AND n.created < %d", $this->id, FEEDS_REQUEST_TIME - $time, 0, variable_get('feeds_node_batch_size', FEEDS_NODE_BATCH_SIZE));
+    $count = $this->getLimit();
+    $result = db_query_range("SELECT n.nid FROM {node} n
+                             JOIN {feeds_item} fi ON fi.entity_type = 'node'
+                             AND n.nid = fi.entity_id WHERE fi.id = '%s'
+                             AND n.created < %d", $this->id, FEEDS_REQUEST_TIME - $time, 0, $count);
+    $nids = array();
     while ($node = db_fetch_object($result)) {
-      _feeds_node_delete($node->nid);
+      $nids[$node->nid] = $node->nid;
+    }
+    foreach ($nodes as $node) {
+      $nids[$node->nid] = $node->nid;
     }
-    if (db_result(db_query_range("SELECT n.nid FROM {node} n JOIN {feeds_node_item} fni ON n.nid = fni.nid WHERE fni.id = '%s' AND n.created < %d", $this->id, FEEDS_REQUEST_TIME - $time, 0, 1))) {
+    $this->entityDeleteMultiple($nids);
+    if (db_query_range("SELECT 1 FROM {node} n JOIN {feeds_item} fi ON fi.entity_type = 'node'
+                       AND n.nid = fi.entity_id WHERE fi.id = '%s' AND n.created < %d",
+                       $this->id, FEEDS_REQUEST_TIME - $time, 0, 1)) {
       return FEEDS_BATCH_ACTIVE;
     }
     return FEEDS_BATCH_COMPLETE;
@@ -145,12 +145,11 @@ class FeedsNodeProcessor extends FeedsPr
     $type = isset($types['story']) ? 'story' : key($types);
     return array(
       'content_type' => $type,
-      'input_format' => FILTER_FORMAT_DEFAULT,
-      'update_existing' => FEEDS_SKIP_EXISTING,
       'expire' => FEEDS_EXPIRE_NEVER,
-      'mappings' => array(),
       'author' => 0,
-    );
+      'input_format' => FILTER_FORMAT_DEFAULT,
+      'delete_with_source' => FALSE,
+    ) + parent::configDefaults();
   }
 
   /**
@@ -158,7 +157,8 @@ class FeedsNodeProcessor extends FeedsPr
    */
   public function configForm(&$form_state) {
     $types = node_get_types('names');
-    $form = array();
+    array_walk($types, 'check_plain');
+    $form = parent::configForm($form_state);
     $form['content_type'] = array(
       '#type' => 'select',
       '#title' => t('Content type'),
@@ -166,19 +166,20 @@ class FeedsNodeProcessor extends FeedsPr
       '#options' => $types,
       '#default_value' => $this->config['content_type'],
     );
-    $format_options = array(FILTER_FORMAT_DEFAULT => t('Default format'));
     $formats = filter_formats();
-      foreach ($formats as $format) {
-        $format_options[$format->format] = $format->name;
-      }
+    $format_options = array(FILTER_FORMAT_DEFAULT => t('Default format'));
+    foreach ($formats as $format) {
+      $format_options[$format->format] = check_plain($format->name);
+    }
     $form['input_format'] = array(
       '#type' => 'select',
-      '#title' => t('Input format'),
+      '#title' => t('Text format'),
       '#description' => t('Select the input format for the body field of the nodes to be created.'),
       '#options' => $format_options,
       '#default_value' => $this->config['input_format'],
+      '#required' => TRUE,
     );
-    $author = user_load(array('uid' => $this->config['author']));
+    $author = user_load($this->config['author']);
     $form['author'] = array(
       '#type' => 'textfield',
       '#title' => t('Author'),
@@ -194,16 +195,10 @@ class FeedsNodeProcessor extends FeedsPr
       '#description' => t('Select after how much time nodes should be deleted. The node\'s published date will be used for determining the node\'s age, see Mapping settings.'),
       '#default_value' => $this->config['expire'],
     );
-    $form['update_existing'] = array(
-      '#type' => 'radios',
-      '#title' => t('Update existing nodes'),
-      '#description' => t('Select how existing nodes should be updated. Existing nodes will be determined using mappings that are a "unique target".'),
-      '#options' => array(
-        FEEDS_SKIP_EXISTING => 'Do not update existing nodes',
-        FEEDS_REPLACE_EXISTING => 'Replace existing nodes',
-        FEEDS_UPDATE_EXISTING => 'Update existing nodes (slower than replacing them)',
-      ),
-      '#default_value' => $this->config['update_existing'],
+    $form['update_existing']['#options'] = array(
+      FEEDS_SKIP_EXISTING => 'Do not update existing nodes',
+      FEEDS_REPLACE_EXISTING => 'Replace existing nodes',
+      FEEDS_UPDATE_EXISTING => 'Update existing nodes (slower than replacing them)',
     );
     return $form;
   }
@@ -212,6 +207,7 @@ class FeedsNodeProcessor extends FeedsPr
    * Override parent::configFormValidate().
    */
   public function configFormValidate(&$values) {
+    drupal_set_message(var_export($values, TRUE));
     if ($author = user_load(array('name' => $values['author']))) {
       $values['author'] = $author->uid;
     }
@@ -233,16 +229,28 @@ class FeedsNodeProcessor extends FeedsPr
   /**
    * Override setTargetElement to operate on a target item that is a node.
    */
-  public function setTargetElement($target_node, $target_element, $value) {
-    if (in_array($target_element, array('url', 'guid'))) {
-      $target_node->feeds_node_item->$target_element = $value;
-    }
-    elseif ($target_element == 'body') {
-      $target_node->teaser = node_teaser($value);
-      $target_node->body = $value;
-    }
-    elseif (in_array($target_element, array('title', 'status', 'created', 'nid', 'uid'))) {
-      $target_node->$target_element = $value;
+  public function setTargetElement(FeedsSource $source, $target_node, $target_element, $value) {
+    switch ($target_element) {
+      case 'created':
+        $target_node->created = feeds_to_unixtime($value, FEEDS_REQUEST_TIME);
+        break;
+      case 'feeds_source':
+        // Get the class of the feed node importer's fetcher and set the source
+        // property. See feeds_node_update() how $node->feeds gets stored.
+        if ($id = feeds_get_importer_id($this->config['content_type'])) {
+          $class = get_class(feeds_importer($id)->fetcher);
+          $target_node->feeds[$class]['source'] = $value;
+          // This effectively suppresses 'import on submission' feature.
+          // See feeds_node_insert().
+          $target_node->feeds['suppress_import'] = TRUE;
+        }
+        break;
+      case 'body':
+        $target_node->teaser = node_teaser($value);
+        $target_node->body = $value;
+      default:
+        parent::setTargetElement($source, $target_node, $target_element, $value);
+        break;
     }
   }
 
@@ -250,14 +258,14 @@ class FeedsNodeProcessor extends FeedsPr
    * Return available mapping targets.
    */
   public function getMappingTargets() {
-    $targets = array(
-      'title' => array(
+    $type = node_get_types('type',  $this->config['content_type']);
+    $targets = parent::getMappingTargets();
+    if ($type->has_title) {
+      $targets['title'] = array(
         'name' => t('Title'),
         'description' => t('The title of the node.'),
-       ),
-     );
-    // Include body field only if available.
-    $type = node_get_types('type',  $this->config['content_type']);
+      );
+    }
     if ($type->has_body) {
       // Using 'teaser' instead of 'body' forces entire content above the break.
       $targets['body'] = array(
@@ -283,42 +291,43 @@ class FeedsNodeProcessor extends FeedsPr
         'name' => t('Published date'),
         'description' => t('The UNIX time when a node has been published.'),
       ),
-      'url' => array(
-        'name' => t('URL'),
-        'description' => t('The external URL of the node. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
-        'optional_unique' => TRUE,
-      ),
-      'guid' => array(
-        'name' => t('GUID'),
-        'description' => t('The external GUID of the node. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
-        'optional_unique' => TRUE,
-      ),
     );
-
+    // If the target content type is a Feed node, expose its source field.
+    if ($id = feeds_get_importer_id($this->config['content_type'])) {
+      $name = feeds_importer($id)->config['name'];
+      $targets['feeds_source'] = array(
+        'name' => t('Feed source'),
+        'description' => t('The content type created by this processor is a Feed Node, it represents a source itself. Depending on the fetcher selected on the importer "@importer", this field is expected to be for example a URL or a path to a file.', array('@importer' => $name)),
+        'optional_unique' => TRUE,
+      );
+    }
     // Let other modules expose mapping targets.
     self::loadMappers();
-    drupal_alter('feeds_node_processor_targets', $targets, $this->config['content_type']);
-
+    feeds_alter('feeds_processor_targets', $targets, 'node', $this->config['content_type']);
     return $targets;
   }
 
   /**
    * Get nid of an existing feed item node if available.
    */
-  protected function existingItemId(FeedsImportBatch $batch, FeedsSource $source) {
+  protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
+    if ($nid = parent::existingEntityId($source, $result)) {
+      return $nid;
+    }
 
     // Iterate through all unique targets and test whether they do already
     // exist in the database.
-    foreach ($this->uniqueTargets($batch) as $target => $value) {
+    foreach ($this->uniqueTargets($source, $result) as $target => $value) {
       switch ($target) {
         case 'nid':
-          $nid = db_result(db_query("SELECT nid FROM {node} WHERE nid = %d", $value));
-          break;
-        case 'url':
-          $nid = db_result(db_query("SELECT nid FROM {feeds_node_item} WHERE feed_nid = %d AND id = '%s' AND url = '%s'", $source->feed_nid, $source->id, $value));
+          $nid = db_fetch_object(db_query("SELECT nid FROM {node} WHERE nid = %d", $value))->nid;
           break;
-        case 'guid':
-          $nid = db_result(db_query("SELECT nid FROM {feeds_node_item} WHERE feed_nid = %d AND id = '%s' AND guid = '%s'", $source->feed_nid, $source->id, $value));
+        case 'feeds_source':
+          if ($id = feeds_get_importer_id($this->config['content_type'])) {
+            $nid = db_fetch_object(db_query("SELECT fs.feed_nid FROM {node} n
+                            JOIN {feeds_source} fs ON n.nid = fs.feed_nid
+                            WHERE fs.id = '%s' AND fs.source = '%s'", $id, $value))->feed_nid;
+          }
           break;
       }
       if ($nid) {
@@ -328,108 +337,4 @@ class FeedsNodeProcessor extends FeedsPr
     }
     return 0;
   }
-
-  /**
-   * Creates a new node object in memory and returns it.
-   */
-  protected function buildNode($nid, $feed_nid) {
-    $node = new stdClass();
-    if (empty($nid)) {
-      $node->created = FEEDS_REQUEST_TIME;
-      $populate = TRUE;
-    }
-    else {
-      if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
-        $node = node_load($nid, NULL, TRUE);
-      }
-      else {
-        $node->nid = $nid;
-        $node->vid = db_result(db_query("SELECT vid FROM {node} WHERE nid = %d", $nid));
-        $populate = TRUE;
-      }
-    }
-    if ($populate) {
-      $node->type = $this->config['content_type'];
-      $node->changed = FEEDS_REQUEST_TIME;
-      $node->format = $this->config['input_format'];
-      $node->feeds_node_item = new stdClass();
-      $node->feeds_node_item->id = $this->id;
-      $node->feeds_node_item->imported = FEEDS_REQUEST_TIME;
-      $node->feeds_node_item->feed_nid = $feed_nid;
-      $node->feeds_node_item->url = '';
-      $node->feeds_node_item->guid = '';
-    }
-
-    static $included;
-    if (!$included) {
-      module_load_include('inc', 'node', 'node.pages');
-      $included = TRUE;
-    }
-    node_object_prepare($node);
-
-    // Populate properties that are set by node_object_prepare().
-    $node->log = 'Created/updated by FeedsNodeProcessor';
-    if ($populate) {
-      $node->uid = $this->config['author'];
-    }
-    return $node;
-  }
-
-  /**
-   * Create MD5 hash of item and mappings array.
-   *
-   * Include mappings as a change in mappings may have an affect on the item
-   * produced.
-   *
-   * @return Always returns a hash, even with empty, NULL, FALSE:
-   *  Empty arrays return 40cd750bba9870f18aada2478b24840a
-   *  Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
-   */
-  protected function hash($item) {
-    static $serialized_mappings;
-    if (!$serialized_mappings) {
-      $serialized_mappings = serialize($this->config['mappings']);
-    }
-    return hash('md5', serialize($item) . $serialized_mappings);
-  }
-
-  /**
-   * Retrieve MD5 hash of $nid from DB.
-   * @return Empty string if no item is found, hash otherwise.
-   */
-  protected function getHash($nid) {
-    $hash = db_result(db_query("SELECT hash FROM {feeds_node_item} WHERE nid = %d", $nid));
-    if ($hash) {
-      // Return with the hash.
-      return $hash;
-    }
-    return '';
-  }
-}
-
-/**
- * Copy of node_delete() that circumvents node_access().
- *
- * Used for batch deletion.
- */
-function _feeds_node_delete($nid) {
-
-  $node = node_load($nid);
-
-  db_query("DELETE FROM {node} WHERE nid = %d", $node->nid);
-  db_query("DELETE FROM {node_revisions} WHERE nid = %d", $node->nid);
-
-  // Call the node-specific callback (if any):
-  node_invoke($node, 'delete');
-  node_invoke_nodeapi($node, 'delete');
-
-  // Clear the page and block caches.
-  cache_clear_all();
-
-  // Remove this node from the search index if needed.
-  if (function_exists('search_wipe')) {
-    search_wipe($node->nid, 'node');
-  }
-  watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title));
-  drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
 }
Index: plugins/FeedsOPMLParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsOPMLParser.inc,v
retrieving revision 1.6.2.1
diff -u -p -r1.6.2.1 FeedsOPMLParser.inc
--- plugins/FeedsOPMLParser.inc	25 Oct 2010 22:43:03 -0000	1.6.2.1
+++ plugins/FeedsOPMLParser.inc	3 Feb 2011 13:04:01 -0000
@@ -14,11 +14,12 @@ class FeedsOPMLParser extends FeedsParse
   /**
    * Implementation of FeedsParser::parse().
    */
-  public function parse(FeedsImportBatch $batch, FeedsSource $source) {
+  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
     feeds_include_library('opml_parser.inc', 'opml_parser');
-    $result = opml_parser_parse($batch->getRaw());
-    $batch->title = $result['title'];
-    $batch->items = $result['items'];
+    $opml = opml_parser_parse($fetcher_result->getRaw());
+    $result = new FeedsParserResult($opml['items']);
+    $result->title = $opml['title'];
+    return $result;
   }
 
   /**
Index: plugins/FeedsParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsParser.inc,v
retrieving revision 1.22.2.4
diff -u -p -r1.22.2.4 FeedsParser.inc
--- plugins/FeedsParser.inc	25 Oct 2010 22:53:53 -0000	1.22.2.4
+++ plugins/FeedsParser.inc	3 Feb 2011 13:04:02 -0000
@@ -2,6 +2,49 @@
 // $Id: FeedsParser.inc,v 1.22.2.4 2010/10/25 22:53:53 alexb Exp $
 
 /**
+ * A result of a parsing stage.
+ */
+class FeedsParserResult extends FeedsResult {
+  public $title;
+  public $description;
+  public $link;
+  public $items;
+  public $current_item;
+
+  /**
+   * Constructor.
+   */
+  public function __construct($items = array()) {
+    $this->title = '';
+    $this->description = '';
+    $this->link = '';
+    $this->items = $items;
+  }
+
+  /**
+   * @todo Move to a nextItem() based approach, not consuming the item array.
+   *   Can only be done once we don't cache the entire batch object between page
+   *   loads for batching anymore.
+   *
+   * @return
+   *   Next available item or NULL if there is none. Every returned item is
+   *   removed from the internal array.
+   */
+  public function shiftItem() {
+    $this->current_item = array_shift($this->items);
+    return $this->current_item;
+  }
+
+  /**
+   * @return
+   *   Current result item.
+   */
+  public function currentItem() {
+    return empty($this->current_item) ? NULL : $this->current_item;
+  }
+}
+
+/**
  * Abstract class, defines interface for parsers.
  */
 abstract class FeedsParser extends FeedsPlugin {
@@ -11,12 +54,12 @@ abstract class FeedsParser extends Feeds
    *
    * Extending classes must implement this method.
    *
-   * @param $batch
-   *   FeedsImportBatch returned by fetcher.
    * @param FeedsSource $source
    *   Source information.
+   * @param $fetcher_result
+   *   FeedsFetcherResult returned by fetcher.
    */
-  public abstract function parse(FeedsImportBatch $batch, FeedsSource $source);
+  public abstract function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result);
 
   /**
    * Clear all caches for results for given source.
@@ -49,7 +92,7 @@ abstract class FeedsParser extends Feeds
   public function getMappingSources() {
     self::loadMappers();
     $sources = array();
-    drupal_alter('feeds_parser_sources', $sources, feeds_importer($this->id)->config['content_type']);
+    feeds_alter('feeds_parser_sources', $sources, feeds_importer($this->id)->config['content_type']);
     if (!feeds_importer($this->id)->config['content_type']) {
       return $sources;
     }
@@ -81,11 +124,13 @@ abstract class FeedsParser extends Feeds
    * @see FeedsProcessor::map()
    * @see FeedsCSVParser::getSourceElement().
    */
-  public function getSourceElement(FeedsImportBatch $batch, $element_key) {
-    if (($node = node_load($batch->feed_nid)) && $element_key == 'parent:uid') {
+  public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
+    if ($element_key == 'parent:uid' &&
+        $source->feed_nid &&
+        ($node = node_load($source->feed_nid))) {
       return $node->uid;
     }
-    $item = $batch->currentItem();
+    $item = $result->currentItem();
     return isset($item[$element_key]) ? $item[$element_key] : '';
   }
 }
@@ -112,13 +157,16 @@ class FeedsElement {
    * @todo Make value public and deprecate use of getValue().
    *
    * @return
-   *   Standard value of this FeedsElement.
+   *   Value of this FeedsElement represented as a scalar.
    */
   public function getValue() {
     return $this->value;
   }
 
   /**
+   * Magic method __toString() for printing and string conversion of this
+   * object.
+   *
    * @return
    *   A string representation of this element.
    */
@@ -313,20 +361,16 @@ class FeedsDateTimeElement extends Feeds
 
   /**
    * Override FeedsElement::getValue().
+   *
+   * @return
+   *   The UNIX timestamp of this object's start date. Return value is
+   *   technically a string but will only contain numeric values.
    */
   public function getValue() {
-    return $this->start;
-  }
-
-  /**
-   * Implementation of toString magic php method.
-   */
-  public function __toString() {
-    $val = $this->getValue();
-    if ($val) {
-      return $val->format('U');
+    if ($this->start) {
+      return $this->start->format('U');
     }
-    return '';
+    return '0';
   }
 
   /**
@@ -363,36 +407,36 @@ class FeedsDateTimeElement extends Feeds
    * Helper method for buildDateField(). Build a FeedsDateTimeElement object
    * from a standard formatted node.
    */
-  protected static function readDateField($node, $field_name) {
-    $field = content_fields($field_name);
+  protected static function readDateField($entity, $field_name) {
     $ret = new FeedsDateTimeElement();
-    if (isset($node->{$field_name}[0]['date']) && $node->{$field_name}[0]['date'] instanceof FeedsDateTime) {
-      $ret->start = $node->{$field_name}[0]['date'];
+    if (isset($entity->{$field_name}[0]['date']) && $entity->{$field_name}[0]['date'] instanceof FeedsDateTime) {
+      $ret->start = $entity->{$field_name}[0]['date'];
     }
-    if (isset($node->{$field_name}[0]['date2']) && $node->{$field_name}[0]['date2'] instanceof FeedsDateTime) {
-      $ret->end = $node->{$field_name}[0]['date2'];
+    if (isset($entity->{$field_name}[0]['date2']) && $entity->{$field_name}[0]['date2'] instanceof FeedsDateTime) {
+      $ret->end = $entity->{$field_name}[0]['date2'];
     }
     return $ret;
   }
 
   /**
-   * Build a node's date CCK field from our object.
+   * Build a entity's date field from our object.
    *
-   * @param $node
-   *   The node to build the date field on.
+   * @param $entity
+   *   The entity to build the date field on.
    * @param $field_name
    *   The name of the field to build.
    */
-  public function buildDateField($node, $field_name) {
-    $field = content_fields($field_name);
-    $oldfield = FeedsDateTimeElement::readDateField($node, $field_name);
+  public function buildDateField($entity, $field_name) {
+    $info = content_fields($field_name);
+
+    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name);
     // Merge with any preexisting objects on the field; we take precedence.
     $oldfield = $this->merge($oldfield);
     $use_start = $oldfield->start;
     $use_end = $oldfield->end;
 
     // Set timezone if not already in the FeedsDateTime object
-    $to_tz = date_get_timezone($field['tz_handling'], date_default_timezone_name());
+    $to_tz = date_get_timezone($info['tz_handling'], date_default_timezone());
     $temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
 
     $db_tz = '';
@@ -401,7 +445,7 @@ class FeedsDateTimeElement extends Feeds
       if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
         $use_start->setTimezone(new DateTimeZone("UTC"));
       }
-      $db_tz = date_get_timezone_db($field['tz_handling'], $use_start->getTimezone()->getName());
+      $db_tz = date_get_timezone_db($info['tz_handling'], $use_start->getTimezone()->getName());
     }
     if ($use_end) {
       $use_end = $use_end->merge($temp);
@@ -409,7 +453,7 @@ class FeedsDateTimeElement extends Feeds
         $use_end->setTimezone(new DateTimeZone("UTC"));
       }
       if (!$db_tz) {
-        $db_tz = date_get_timezone_db($field['tz_handling'], $use_end->getTimezone()->getName());
+        $db_tz = date_get_timezone_db($info['tz_handling'], $use_end->getTimezone()->getName());
       }
     }
     if (!$db_tz) {
@@ -417,28 +461,28 @@ class FeedsDateTimeElement extends Feeds
     }
 
     $db_tz = new DateTimeZone($db_tz);
-    if (!isset($node->{$field_name})) {
-      $node->{$field_name} = array();
+    if (!isset($entity->{$field_name})) {
+      $entity->{$field_name} = array();
     }
     if ($use_start) {
-      $node->{$field_name}[0]['timezone'] = $use_start->getTimezone()->getName();
-      $node->{$field_name}[0]['offset'] = $use_start->getOffset();
+      $entity->{$field_name}[0]['timezone'] = $use_start->getTimezone()->getName();
+      $entity->{$field_name}[0]['offset'] = $use_start->getOffset();
       $use_start->setTimezone($db_tz);
-      $node->{$field_name}[0]['date'] = $use_start;
+      $entity->{$field_name}[0]['date'] = $use_start;
       /**
        * @todo the date_type_format line could be simplified based upon a patch
        *   DO issue #259308 could affect this, follow up on at some point.
        *   Without this, all granularity info is lost.
        *   $use_start->format(date_type_format($field['type'], $use_start->granularity));
        */
-      $node->{$field_name}[0]['value'] = $use_start->format(date_type_format($field['type']));
+      $entity->{$field_name}[0]['value'] = $use_start->format(date_type_format($info['type']));
     }
     if ($use_end) {
       // Don't ever use end to set timezone (for now)
-      $node->{$field_name}[0]['offset2'] = $use_end->getOffset();
+      $entity->{$field_name}[0]['offset2'] = $use_end->getOffset();
       $use_end->setTimezone($db_tz);
-      $node->{$field_name}[0]['date2'] = $use_end;
-      $node->{$field_name}[0]['value2'] = $use_end->format(date_type_format($field['type']));
+      $entity->{$field_name}[0]['date2'] = $use_end;
+      $entity->{$field_name}[0]['value2'] = $use_end->format(date_type_format($info['type']));
     }
   }
 }
@@ -482,7 +526,8 @@ class FeedsDateTime extends DateTime {
    * Overridden constructor.
    *
    * @param $time
-   *   time string, flexible format including timestamp.
+   *   time string, flexible format including timestamp. Invalid formats will
+   *   fall back to 'now'.
    * @param $tz
    *   PHP DateTimeZone object, NULL allowed
    */
@@ -631,3 +676,28 @@ class FeedsDateTime extends DateTime {
     return array('year' => $this->format('Y'), 'month' => $this->format('m'), 'day' => $this->format('d'), 'hour' => $this->format('H'), 'minute' => $this->format('i'), 'second' => $this->format('s'), 'zone' => $this->format('e'));
   }
 }
+
+/**
+ * Converts to UNIX time.
+ *
+ * @param $date
+ *   A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
+ * @param $default_value
+ *   A default UNIX timestamp to return if $date could not be parsed.
+ *
+ * @return
+ *   $date as UNIX time if conversion was successful, $dfeault_value otherwise.
+ */
+function feeds_to_unixtime($date, $default_value) {
+  if (is_numeric($date)) {
+    return $date;
+  }
+  elseif (is_string($date)) {
+    $date = new FeedsDateTimeElement($date);
+    return $date->getValue();
+  }
+  elseif ($date instanceof FeedsDateTimeElement) {
+    return $date->getValue();
+  }
+  return $default_value;
+}
Index: plugins/FeedsPlugin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsPlugin.inc,v
retrieving revision 1.6
diff -u -p -r1.6 FeedsPlugin.inc
--- plugins/FeedsPlugin.inc	7 Sep 2010 17:29:36 -0000	1.6
+++ plugins/FeedsPlugin.inc	3 Feb 2011 13:04:02 -0000
@@ -5,6 +5,12 @@
  * @file
  * Definition of FeedsPlugin class.
  */
+require_once(dirname(__FILE__) .'/../includes/FeedsConfigurable.inc');
+require_once(dirname(__FILE__) .'/../includes/FeedsSource.inc');
+/**
+ * Base class for a fetcher, parser or processor result.
+ */
+class FeedsResult {}
 
 /**
  * Implement source interface for all plugins.
@@ -83,22 +89,116 @@ abstract class FeedsPlugin extends Feeds
    *
    * @see FeedsNodeProcessor::map()
    * @see FeedsUserProcessor::map()
+   *
+   * @todo: Use CTools Plugin API.
    */
   protected static function loadMappers() {
     static $loaded = FALSE;
     if (!$loaded) {
       $path = drupal_get_path('module', 'feeds') .'/mappers';
-      $files = drupal_system_listing('.*\.inc$', $path, 'name', 0);
+      $files = drupal_system_listing('/.*\.inc$/', $path, 'name', 0);
       foreach ($files as $file) {
-        if (strstr($file->filename, '/mappers/')) {
-          require_once("./$file->filename");
+        if (strstr($file->uri, '/mappers/')) {
+          require_once("./$file->uri");
         }
       }
-      // Rebuild cache.
-      module_implements('', FALSE, TRUE);
     }
     $loaded = TRUE;
   }
+
+  /**
+   * Get all available plugins.
+   */
+  public static function all() {
+    ctools_include('plugins');
+    $plugins = ctools_get_plugins('feeds', 'plugins');
+
+    $result = array();
+    foreach ($plugins as $key => $info) {
+      if (!empty($info['hidden'])) {
+        continue;
+      }
+      $result[$key] = $info;
+    }
+
+    // Sort plugins by name and return.
+    uasort($result, 'feeds_plugin_compare');
+    return $result;
+  }
+
+  /**
+   * Determines whether given plugin is derived from given base plugin.
+   *
+   * @param $plugin_key
+   *   String that identifies a Feeds plugin key.
+   * @param $parent_plugin
+   *   String that identifies a Feeds plugin key to be tested against.
+   *
+   * @return
+   *   TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin,
+   *   FALSE otherwise.
+   */
+  public static function child($plugin_key, $parent_plugin) {
+    ctools_include('plugins');
+    $plugins = ctools_get_plugins('feeds', 'plugins');
+    $info = $plugins[$plugin_key];
+
+    if (empty($info['handler']['parent'])) {
+      return FALSE;
+    }
+    elseif ($info['handler']['parent'] == $parent_plugin) {
+      return TRUE;
+    }
+    else {
+      return self::child($info['handler']['parent'], $parent_plugin);
+    }
+  }
+
+  /**
+   * Determines the type of a plugin.
+   *
+   * @todo PHP5.3: Implement self::type() and query with $plugin_key::type().
+   *
+   * @param $plugin_key
+   *   String that identifies a Feeds plugin key.
+   *
+   * @return
+   *   One of the following values:
+   *   'fetcher' if the plugin is a fetcher
+   *   'parser' if the plugin is a parser
+   *   'processor' if the plugin is a processor
+   *   FALSE otherwise.
+   */
+  public static function typeOf($plugin_key) {
+    if (self::child($plugin_key, 'FeedsFetcher')) {
+      return 'fetcher';
+    }
+    elseif (self::child($plugin_key, 'FeedsParser')) {
+      return 'parser';
+    }
+    elseif (self::child($plugin_key, 'FeedsProcessor')) {
+      return 'processor';
+    }
+    return FALSE;
+  }
+
+  /**
+   * Gets all available plugins of a particular type.
+   *
+   * @param $type
+   *   'fetcher', 'parser' or 'processor'
+   */
+  public static function byType($type) {
+    $plugins = self::all();
+
+    $result = array();
+    foreach ($plugins as $key => $info) {
+      if ($type == self::typeOf($key)) {
+        $result[$key] = $info;
+      }
+    }
+    return $result;
+  }
 }
 
 /**
@@ -109,3 +209,10 @@ class FeedsMissingPlugin extends FeedsPl
     return array();
   }
 }
+
+/**
+ * Sort callback for FeedsPlugin::all().
+ */
+function feeds_plugin_compare($a, $b) {
+  return strcasecmp($a['name'], $b['name']);
+}
Index: plugins/FeedsProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsProcessor.inc,v
retrieving revision 1.18
diff -u -p -r1.18 FeedsProcessor.inc
--- plugins/FeedsProcessor.inc	10 Sep 2010 15:44:56 -0000	1.18
+++ plugins/FeedsProcessor.inc	3 Feb 2011 13:04:03 -0000
@@ -7,29 +7,207 @@ define('FEEDS_SKIP_EXISTING', 0);
 define('FEEDS_REPLACE_EXISTING', 1);
 define('FEEDS_UPDATE_EXISTING', 2);
 
+// Default limit for creating items on a page load, not respected by all
+// processors.
+define('FEEDS_PROCESS_LIMIT', 50);
+
+/**
+ * Thrown if a validation fails.
+ */
+class FeedsValidationException extends Exception {}
+
 /**
  * Abstract class, defines interface for processors.
  */
 abstract class FeedsProcessor extends FeedsPlugin {
+  /**
+   * @defgroup entity_api_wrapper Entity API wrapper.
+   */
+
+  /**
+   * Entity type this processor operates on.
+   */
+  public abstract function entityType();
+
+  /**
+   * Create a new entity.
+   *
+   * @param $source
+   *   The feeds source that spawns this entity.
+   *
+   * @return
+   *   A new entity object.
+   */
+  protected abstract function newEntity(FeedsSource $source);
+
+  /**
+   * Load an existing entity.
+   *
+   * @param $source
+   *   The feeds source that spawns this entity.
+   * @param $entity_id
+   *   The unique id of the entity that should be loaded.
+   *
+   * @return
+   *   A new entity object.
+   */
+  protected abstract function entityLoad(FeedsSource $source, $entity_id);
+
+  /**
+   * Validate an entity.
+   *
+   * @throws FeedsValidationException $e
+   *   If validation fails.
+   */
+  protected function entityValidate($entity) {}
+
+  /**
+   * Save an entity.
+   *
+   * @param $entity
+   *   Entity to b saved.
+   */
+  protected abstract function entitySave($entity);
 
   /**
-   * Process the result of the parser or previous processors.
-   * Extending classes must implement this method.
+   * Delete a series of entities.
+   *
+   * @param $entity_ids
+   *   Array of unique identity ids to be deleted.
+   */
+  protected abstract function entityDeleteMultiple($entity_ids);
+
+  /**
+   * Wrap entity_get_info() into a method so that extending classes can override
+   * it and more entity information. Allowed additional keys:
+   *
+   * 'label plural' ... the plural label of an entity type.
+   */
+  protected function entityInfo() {
+    return entity_get_info($this->entityType());
+  }
+
+  /**
+   * @}
+   */
+
+  /**
+   * Process the result of the parsing stage.
    *
-   * @param FeedsImportBatch $batch
-   *   The current feed import data passed in from the parsing stage.
    * @param FeedsSource $source
    *   Source information about this import.
+   * @param FeedsParserResult $parser_result
+   *   The result of the parsing stage.
    */
-  public abstract function process(FeedsImportBatch $batch, FeedsSource $source);
+  public function process(FeedsSource $source, FeedsParserResult $parser_result) {
+    $state = $source->state(FEEDS_PROCESS);
+
+    while ($item = $parser_result->shiftItem()) {
+      if (!($entity_id = $this->existingEntityId($source, $parser_result)) ||
+           ($this->config['update_existing'] != FEEDS_SKIP_EXISTING)) {
+
+        // Only proceed if item has actually changed.
+        $hash = $this->hash($item);
+        if (!empty($entity_id) && $hash == $this->getHash($entity_id)) {
+          continue;
+        }
+
+        try {
+          // Assemble node, map item to it, save.
+          if (empty($entity_id)) {
+            $entity = $this->newEntity($source);
+            $this->newItemInfo($entity, $source->feed_nid, $hash);
+          }
+          else {
+            $entity = $this->entityLoad($source, $entity_id);
+            // If an existing item info can't be loaded, create one.
+            if (!$this->loadItemInfo($entity)) {
+              $this->newItemInfo($entity, $source->feed_nid, $hash);
+              $entity->feeds_item->entity_id = $entity_id;
+            }
+          }
+          $entity->feeds_item->hash = $hash;
+          $this->map($source, $parser_result, $entity);
+          $this->entityValidate($entity);
+          $this->entitySave($entity);
+
+          // Track progress.
+          if (empty($entity_id)) {
+            $state->created++;
+          }
+          else {
+            $state->updated++;
+          }
+        }
+        catch (Exception $e) {
+          $state->failed++;
+          drupal_set_message($e->getMessage(), 'warning');
+          $message = $e->getMessage();
+          $message .= '<h3>Original item</h3>';
+          $message .= '<pre>' . var_export($item, true) . '</pre>';
+          $message .= '<h3>Entity</h3>';
+          $message .= '<pre>' . var_export($entity, true) . '</pre>';
+          $source->log('import', $message, array(), WATCHDOG_ERROR);
+        }
+      }
+    }
+
+    // Set messages if we're done.
+    if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
+      return;
+    }
+    $info = $this->entityInfo();
+    $tokens = array(
+      '@entity' => strtolower($info['label']),
+      '@entities' => strtolower($info['label plural']),
+    );
+    $messages = array();
+    if ($state->created) {
+      $messages[] = array(
+       'message' => format_plural(
+          $state->created,
+          'Created @number @entity.',
+          'Created @number @entities.',
+          array('@number' => $state->created) + $tokens
+        ),
+      );
+    }
+    if ($state->updated) {
+      $messages[] = array(
+       'message' => format_plural(
+          $state->updated,
+          'Updated @number @entity.',
+          'Updated @number @entities.',
+          array('@number' => $state->updated) + $tokens
+        ),
+      );
+    }
+    if ($state->failed) {
+      $messages[] = array(
+       'message' => format_plural(
+          $state->failed,
+          'Failed importing @number @entity.',
+          'Failed importing @number @entities.',
+          array('@number' => $state->failed) + $tokens
+        ),
+        'level' => WATCHDOG_ERROR,
+      );
+    }
+    if (empty($messages)) {
+      $messages[] = array(
+        'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
+      );
+    }
+    foreach ($messages as $message) {
+      drupal_set_message($message['message']);
+      $source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
+    }
+  }
 
   /**
-   * Remove all stored results or stored results up to a certain time for this
-   * configuration/this source.
+   * Remove all stored results or stored results up to a certain time for a
+   * source.
    *
-   * @param FeedsBatch $batch
-   *   A FeedsBatch object for tracking information such as how many
-   *   items have been deleted total between page loads.
    * @param FeedsSource $source
    *   Source information for this expiry. Implementers should only delete items
    *   pertaining to this source. The preferred way of determining whether an
@@ -37,7 +215,86 @@ abstract class FeedsProcessor extends Fe
    *   processor's responsibility to store the feed_nid of an imported item in
    *   the processing stage.
    */
-  public abstract function clear(FeedsBatch $batch, FeedsSource $source);
+  public function clear(FeedsSource $source) {
+    $state = $source->state(FEEDS_PROCESS_CLEAR);
+
+    // Build base select statement.
+    $info = $this->entityInfo();
+    $key = $info['entity keys']['id'];
+    $table = $info['base table'];
+    $select = "SELECT COUNT(e.$key) FROM {{$table}} e, {feeds_item} fi WHERE
+    e.$key = fi.entity_id AND fi.entity_type = '{$this->entityType()}'
+    AND fi.feed_nid = %d AND fi.id = '%s'";
+
+
+    // If there is no total, query it.
+    if (!$state->total) {
+      $state->total = db_result(db_query($select, $source->feed_nid, $this->id));
+    }
+
+    $select = "SELECT e.$key FROM {{$table}} e, {feeds_item} fi WHERE
+    e.$key = fi.entity_id AND fi.entity_type = '{$this->entityType()}'
+    AND fi.feed_nid = %d AND fi.id = '%s'";
+    drupal_set_message($select);
+
+    drupal_set_message(sprintf($select, $source->feed_nid, $this->id));
+
+    // Delete a batch of entities.
+    $entity_ids = array();
+
+    $result = db_query_range($select, $source->feed_nid, $this->id, 0, $this->getLimit());
+    while ($entity_id = db_fetch_object($result)) {
+      $entity_ids[$entity_id->{$info['entity keys']['id']}] = $entity_id->{$info['entity keys']['id']};
+    }
+    drupal_set_message(var_export($entity_ids, TRUE));
+    $this->entityDeleteMultiple($entity_ids);
+
+    // Report progress, take into account that we may not have deleted as
+    // many items as we have counted at first.
+    if (count($entity_ids)) {
+      $state->deleted += count($entity_ids);
+      $state->progress($state->total, $state->deleted);
+    }
+    else {
+      $state->progress($state->total, $state->total);
+    }
+
+    // Report results when done.
+    if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
+      if ($state->deleted) {
+        $message = format_plural(
+          $state->deleted,
+          'Deleted @number @entity.',
+          'Deleted @number @entities.',
+          array(
+            '@number' => $state->deleted,
+            '@entity' => strtolower($info['label']),
+            '@entities' => strtolower($info['label plural']),
+          )
+        );
+        $source->log('clear', $message, array(), WATCHDOG_INFO);
+        drupal_set_message($message);
+      }
+      else {
+        drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
+      }
+    }
+  }
+
+  /*
+   * Report number of items that can be processed per call.
+   *
+   * 0 means 'unlimited'.
+   *
+   * If a number other than 0 is given, Feeds parsers that support batching
+   * will only deliver this limit to the processor.
+   *
+   * @see FeedsSource::getLimit()
+   * @see FeedsCSVParser::parse()
+   */
+  public function getLimit() {
+    return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
+  }
 
   /**
    * Delete feed items younger than now - $time. Do not invoke expire on a
@@ -60,6 +317,15 @@ abstract class FeedsProcessor extends Fe
   }
 
   /**
+   * Counts the number of items imported by this processor.
+   */
+  public function itemCount(FeedsSource $source) {
+    return db_result(db_query(
+      "SELECT count(*) FROM {feeds_item} WHERE id = '%s'
+      AND entity_type = '%s' AND feed_nid = %d", $this->id, $this->entityType(), $source->feed_nid));
+  }
+
+  /**
    * Execute mapping on an item.
    *
    * This method encapsulates the central mapping functionality. When an item is
@@ -82,7 +348,7 @@ abstract class FeedsProcessor extends Fe
    * @see hook_feeds_term_processor_targets_alter()
    * @see hook_feeds_user_processor_targets_alter()
    */
-  protected function map(FeedsImportBatch $batch, $target_item = NULL) {
+  protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
 
     // Static cache $targets as getMappingTargets() may be an expensive method.
     static $sources;
@@ -101,10 +367,6 @@ abstract class FeedsProcessor extends Fe
     // Many mappers add to existing fields rather than replacing them. Hence we
     // need to clear target elements of each item before mapping in case we are
     // mapping on a prepopulated item such as an existing node.
-    if (is_array($target_item)) {
-      $target_item = (object)$target_item;
-      $convert_to_array = TRUE;
-    }
     foreach ($this->config['mappings'] as $mapping) {
       if (isset($targets[$mapping['target']]['real_target'])) {
         unset($target_item->{$targets[$mapping['target']]['real_target']});
@@ -113,9 +375,6 @@ abstract class FeedsProcessor extends Fe
         unset($target_item->{$mapping['target']});
       }
     }
-    if ($convert_to_array) {
-      $target_item = (array)$target_item;
-    }
 
     /*
     This is where the actual mapping happens: For every mapping we envoke
@@ -129,25 +388,27 @@ abstract class FeedsProcessor extends Fe
     self::loadMappers();
     foreach ($this->config['mappings'] as $mapping) {
       // Retrieve source element's value from parser.
-      if (is_array($sources[$this->id][$mapping['source']]) &&
+      if (isset($sources[$this->id][$mapping['source']]) &&
+          is_array($sources[$this->id][$mapping['source']]) &&
           isset($sources[$this->id][$mapping['source']]['callback']) &&
           function_exists($sources[$this->id][$mapping['source']]['callback'])) {
         $callback = $sources[$this->id][$mapping['source']]['callback'];
-        $value = $callback($batch, $mapping['source']);
+        $value = $callback($source, $result, $mapping['source']);
       }
       else {
-        $value = $parser->getSourceElement($batch, $mapping['source']);
+        $value = $parser->getSourceElement($source, $result, $mapping['source']);
       }
 
       // Map the source element's value to the target.
-      if (is_array($targets[$this->id][$mapping['target']]) &&
+      if (isset($targets[$this->id][$mapping['target']]) &&
+          is_array($targets[$this->id][$mapping['target']]) &&
           isset($targets[$this->id][$mapping['target']]['callback']) &&
           function_exists($targets[$this->id][$mapping['target']]['callback'])) {
         $callback = $targets[$this->id][$mapping['target']]['callback'];
-        $callback($target_item, $mapping['target'], $value);
+        $callback($source, $target_item, $mapping['target'], $value);
       }
       else {
-        $this->setTargetElement($target_item, $mapping['target'], $value);
+        $this->setTargetElement($source, $target_item, $mapping['target'], $value);
       }
     }
     return $target_item;
@@ -165,7 +426,31 @@ abstract class FeedsProcessor extends Fe
    * Declare default configuration.
    */
   public function configDefaults() {
-    return array('mappings' => array());
+    return array(
+      'mappings' => array(),
+      'update_existing' => FEEDS_SKIP_EXISTING,
+    );
+  }
+
+  /**
+   * Overrides parent::configForm().
+   */
+  public function configForm(&$form_state) {
+    $info = $this->entityInfo();
+    $form = array();
+    $tokens = array('@entities' => strtolower($info['label plural']));
+    $form['update_existing'] = array(
+      '#type' => 'radios',
+      '#title' => t('Update existing @entities', $tokens),
+      '#description' =>
+        t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
+      '#options' => array(
+        FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
+        FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
+      ),
+      '#default_value' => $this->config['update_existing'],
+    );
+    return $form;
   }
 
   /**
@@ -186,7 +471,18 @@ abstract class FeedsProcessor extends Fe
    *   FALSE otherwise.
    */
   public function getMappingTargets() {
-    return array();
+    return array(
+      'url' => array(
+        'name' => t('URL'),
+        'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
+        'optional_unique' => TRUE,
+      ),
+      'guid' => array(
+        'name' => t('GUID'),
+        'description' => t('The globally unique identifier of the item. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
+        'optional_unique' => TRUE,
+      ),
+    );
   }
 
   /**
@@ -194,24 +490,69 @@ abstract class FeedsProcessor extends Fe
    *
    * @ingroup mappingapi
    */
-  public function setTargetElement(&$target_item, $target_element, $value) {
-    $target_item[$target_element] = $value;
+  public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
+    switch ($target_element) {
+      case 'url':
+      case 'guid':
+        $target_item->feeds_item->$target_element = $value;
+        break;
+      default:
+        $target_item->$target_element = $value;
+        break;
+    }
   }
 
   /**
-   * Retrieve the target item's existing id if available. Otherwise return 0.
+   * Retrieve the target entity's existing id if available. Otherwise return 0.
    *
    * @ingroup mappingapi
    *
-   * @param $batch
-   *   A FeedsImportBatch object.
    * @param FeedsSource $source
    *   The source information about this import.
+   * @param $result
+   *   A FeedsParserResult object.
+   *
+   * @return
+   *   The serial id of an entity if found, 0 otherwise.
    */
-  protected function existingItemId(FeedsImportBatch $batch, FeedsSource $source) {
+  protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
+
+    // Iterate through all unique targets and test whether they do already
+    // exist in the database.
+    foreach ($this->uniqueTargets($source, $result) as $target => $value) {
+      switch ($target) {
+        case 'url':
+          $entity_id = db_result(db_query("SELECT entity_id FROM {feeds_item}
+                                          WHERE feed_nid = %d
+                                          AND entity_type = '%s'
+                                          AND id = '%s'
+                                          AND url = '%s'",
+                                          $source->feed_nid,
+                                          $this->entityType(),
+                                          $source->id,
+                                          $value));
+          break;
+        case 'guid':
+          $entity_id = db_result(db_query("SELECT entity_id FROM {feeds_item}
+                                          WHERE feed_nid = %d
+                                          AND entity_type = '%s'
+                                          AND id = '%s'
+                                          AND guid = '%s'",
+                                          $source->feed_nid,
+                                          $this->entityType(),
+                                          $source->id,
+                                          $value));
+          break;
+      }
+      if (isset($entity_id)) {
+        // Return with the content id found.
+        return $entity_id;
+      }
+    }
     return 0;
   }
 
+
   /**
    * Utility function that iterates over a target array and retrieves all
    * sources that are unique.
@@ -223,16 +564,90 @@ abstract class FeedsProcessor extends Fe
    *   An array where the keys are target field names and the values are the
    *   elements from the source item mapped to these targets.
    */
-  protected function uniqueTargets(FeedsImportBatch $batch) {
+  protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
     $parser = feeds_importer($this->id)->parser;
     $targets = array();
     foreach ($this->config['mappings'] as $mapping) {
       if ($mapping['unique']) {
         // Invoke the parser's getSourceElement to retrieve the value for this
         // mapping's source.
-        $targets[$mapping['target']] = $parser->getSourceElement($batch, $mapping['source']);
+        $targets[$mapping['target']] = $parser->getSourceElement($source, $result, $mapping['source']);
       }
     }
     return $targets;
   }
+
+  /**
+   * Adds Feeds specific information on $entity->feeds_item.
+   *
+   * @param $entity
+   *   The entity object to be populated with new item info.
+   * @param $feed_nid
+   *   The feed nid of the source that produces this entity.
+   * @param $hash
+   *   The fingerprint of the source item.
+   */
+  protected function newItemInfo($entity, $feed_nid, $hash = '') {
+    $entity->feeds_item = new stdClass();
+    $entity->feeds_item->entity_id = 0;
+    $entity->feeds_item->entity_type = $this->entityType();
+    $entity->feeds_item->id = $this->id;
+    $entity->feeds_item->feed_nid = $feed_nid;
+    $entity->feeds_item->imported = FEEDS_REQUEST_TIME;
+    $entity->feeds_item->hash = $hash;
+    $entity->feeds_item->url = '';
+    $entity->feeds_item->guid = '';
+  }
+
+  /**
+   * Loads existing entity information and places it on $entity->feeds_item.
+   *
+   * @param $entity
+   *   The entity object to load item info for. Id key must be present.
+   *
+   * @return
+   *   TRUE if item info could be loaded, false if not.
+   */
+  protected function loadItemInfo($entity) {
+    $entity_info = $this->entityInfo();
+    $key = $entity_info['entity keys']['id'];
+    if ($item_info = feeds_item_info_load($this->entityType(), $entity->$key)) {
+      $entity->feeds_item = $item_info;
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Create MD5 hash of item and mappings array.
+   *
+   * Include mappings as a change in mappings may have an affect on the item
+   * produced.
+   *
+   * @return Always returns a hash, even with empty, NULL, FALSE:
+   *  Empty arrays return 40cd750bba9870f18aada2478b24840a
+   *  Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
+   */
+  protected function hash($item) {
+    static $serialized_mappings;
+    if (!$serialized_mappings) {
+      $serialized_mappings = serialize($this->config['mappings']);
+    }
+    return hash('md5', serialize($item) . $serialized_mappings);
+  }
+
+  /**
+   * Retrieve MD5 hash of $entity_id from DB.
+   * @return Empty string if no item is found, hash otherwise.
+   */
+  protected function getHash($entity_id) {
+    if ($hash = db_result(db_query("SELECT hash FROM {feeds_item}
+                                         WHERE entity_type = '%s'
+                                         AND entity_id = %d",
+                                         $this->entityType(), $entity_id))) {
+      // Return with the hash.
+      return $hash;
+    }
+    return '';
+  }
 }
Index: plugins/FeedsSimplePieParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsSimplePieParser.inc,v
retrieving revision 1.14.2.1
diff -u -p -r1.14.2.1 FeedsSimplePieParser.inc
--- plugins/FeedsSimplePieParser.inc	25 Oct 2010 22:43:03 -0000	1.14.2.1
+++ plugins/FeedsSimplePieParser.inc	3 Feb 2011 13:04:03 -0000
@@ -60,12 +60,12 @@ class FeedsSimplePieParser extends Feeds
   /**
    * Implementation of FeedsParser::parse().
    */
-  public function parse(FeedsImportBatch $batch, FeedsSource $source) {
+  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
     feeds_include_library('simplepie.inc', 'simplepie');
 
     // Initialize SimplePie.
     $parser = new SimplePie();
-    $parser->set_raw_data($batch->getRaw());
+    $parser->set_raw_data($fetcher_result->getRaw());
     $parser->set_stupidly_fast(TRUE);
     $parser->encode_instead_of_strip(FALSE);
     // @todo Is caching effective when we pass in raw data?
@@ -74,9 +74,10 @@ class FeedsSimplePieParser extends Feeds
     $parser->init();
 
     // Construct the standard form of the parsed feed
-    $batch->title = html_entity_decode(($title = $parser->get_title()) ? $title : $this->createTitle($parser->get_description()));
-    $batch->description = $parser->get_description();
-    $batch->link = html_entity_decode($parser->get_link());
+    $result = new FeedsParserResult();
+    $result->title = html_entity_decode(($title = $parser->get_title()) ? $title : $this->createTitle($parser->get_description()));
+    $result->description = $parser->get_description();
+    $result->link = html_entity_decode($parser->get_link());
 
     $items_num = $parser->get_item_quantity();
     for ($i = 0; $i < $items_num; $i++) {
@@ -136,10 +137,11 @@ class FeedsSimplePieParser extends Feeds
       $this->parseExtensions($item, $simplepie_item);
       $item['raw'] = $simplepie_item->data;
 
-      $batch->items[] = $item;
+      $result->items[] = $item;
     }
     // Release parser.
     unset($parser);
+    return $result;
   }
 
   /**
Index: plugins/FeedsSitemapParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsSitemapParser.inc,v
retrieving revision 1.3.2.2
diff -u -p -r1.3.2.2 FeedsSitemapParser.inc
--- plugins/FeedsSitemapParser.inc	25 Oct 2010 22:43:03 -0000	1.3.2.2
+++ plugins/FeedsSitemapParser.inc	3 Feb 2011 13:04:03 -0000
@@ -8,12 +8,13 @@ class FeedsSitemapParser extends FeedsPa
   /**
    * Implementation of FeedsParser::parse().
    */
-  public function parse(FeedsImportBatch $batch, FeedsSource $source) {
+  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
     // Set time zone to GMT for parsing dates with strtotime().
     $tz = date_default_timezone_get();
     date_default_timezone_set('GMT');
     // Yes, using a DOM parser is a bit inefficient, but will do for now
-    $xml = new SimpleXMLElement($batch->getRaw());
+    $xml = new SimpleXMLElement($fetcher_result->getRaw());
+    $result = new FeedsParserResult();
     foreach ($xml->url as $url) {
       $item = array('url' => (string) $url->loc);
       if ($url->lastmod) {
@@ -25,9 +26,10 @@ class FeedsSitemapParser extends FeedsPa
       if ($url->priority) {
         $item['priority'] = (string) $url->priority;
       }
-      $batch->items[] = $item;
+      $result->items[] = $item;
     }
     date_default_timezone_set($tz);
+    return $result;
   }
 
   /**
Index: plugins/FeedsSyndicationParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsSyndicationParser.inc,v
retrieving revision 1.17.2.1
diff -u -p -r1.17.2.1 FeedsSyndicationParser.inc
--- plugins/FeedsSyndicationParser.inc	25 Oct 2010 22:43:04 -0000	1.17.2.1
+++ plugins/FeedsSyndicationParser.inc	3 Feb 2011 13:04:03 -0000
@@ -11,22 +11,24 @@ class FeedsSyndicationParser extends Fee
   /**
    * Implementation of FeedsParser::parse().
    */
-  public function parse(FeedsImportBatch $batch, FeedsSource $source) {
+  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
     feeds_include_library('common_syndication_parser.inc', 'common_syndication_parser');
-    $result = common_syndication_parser_parse($batch->getRaw());
-    $batch->title = $result['title'];
-    $batch->description = $result['description'];
-    $batch->link = $result['link'];
-    if (is_array($result['items'])) {
-      foreach ($result['items'] as $item) {
+    $feed = common_syndication_parser_parse($fetcher_result->getRaw());
+    $result = new FeedsParserResult();
+    $result->title = $feed['title'];
+    $result->description = $feed['description'];
+    $result->link = $feed['link'];
+    if (is_array($feed['items'])) {
+      foreach ($feed['items'] as $item) {
         if (isset($item['geolocations'])) {
           foreach ($item['geolocations'] as $k => $v) {
             $item['geolocations'][$k] = new FeedsGeoTermElement($v);
           }
         }
-        $batch->items[] = $item;
+        $result->items[] = $item;
       }
     }
+    return $result;
   }
 
   /**
Index: plugins/FeedsTermProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsTermProcessor.inc,v
retrieving revision 1.20.2.2
diff -u -p -r1.20.2.2 FeedsTermProcessor.inc
--- plugins/FeedsTermProcessor.inc	28 Oct 2010 20:48:10 -0000	1.20.2.2
+++ plugins/FeedsTermProcessor.inc	3 Feb 2011 13:04:03 -0000
@@ -10,124 +10,66 @@
  * Feeds processor plugin. Create taxonomy terms from feed items.
  */
 class FeedsTermProcessor extends FeedsProcessor {
-
   /**
-   * Implementation of FeedsProcessor::process().
+   * Define entity type.
    */
-  public function process(FeedsImportBatch $batch, FeedsSource $source) {
-
-    if (empty($this->config['vocabulary'])) {
-      throw new Exception(t('You must define a vocabulary for Taxonomy term processor before importing.'));
-    }
-
-    // Count number of created and updated nodes.
-    $created  = $updated = $no_name = 0;
-
-    while ($item = $batch->shiftItem()) {
-
-      if (!($tid = $this->existingItemId($batch, $source)) || $this->config['update_existing'] != FEEDS_SKIP_EXISTING) {
-
-        // Map item to a term.
-        $term = array();
-        if ($tid && $this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
-          $term = (array) taxonomy_get_term($tid, TRUE);
-          $term = module_invoke_all('feeds_taxonomy_load', $term);
-        }
-        $term = $this->map($batch, $term, $source->feed_nid);
+  public function entityType() {
+    return 'taxonomy_term';
+  }
 
-        // Check if term name is set, otherwise continue.
-        if (empty($term['name'])) {
-          $no_name++;
-          continue;
-        }
+  /**
+   * Implementation of parent::entityInfo().
+   */
+  protected function entityInfo() {
+    $info = array();
+    $info['label'] = t('Term');
+    $info['label plural'] = t('Terms');
+    $info['base table'] = 'term_data';
+    $info['entity keys']['id'] = 'tid';
+    return $info;
+  }
 
-        // Add term id if available.
-        if (!empty($tid)) {
-          $term['tid'] = $tid;
-        }
+  /**
+   * Creates a new term in memory and returns it.
+   */
+  protected function newEntity(FeedsSource $source) {
+    $vocabulary = $this->vocabulary();
+    $term = new stdClass();
+    $term->vid = $vocabulary->vid;
+    return $term;
+  }
 
-        // Save the term.
-        $term['importer_id'] = $this->id;
-        $term['feed_nid'] = $source->feed_nid;
-        taxonomy_save_term($term);
-        if ($tid) {
-          $updated++;
-        }
-        else {
-          $created++;
-        }
-      }
-    }
+  /**
+   * Loads an existing term.
+   */
+  protected function entityLoad(FeedsSource $source, $tid) {
+    return taxonomy_get_term($tid, TRUE);
+  }
 
-    // Set messages.
-    $vocabulary = $this->vocabulary();
-    if ($no_name) {
-      drupal_set_message(
-        format_plural(
-          $no_name,
-          'There was @number term that could not be imported because their name was empty. Check mapping settings on Taxomy term processor.',
-          'There were @number terms that could not be imported because their name was empty. Check mapping settings on Taxomy term processor.',
-          array('@number' => $no_name)
-        ),
-        'error'
-      );
-    }
-    if ($created) {
-      drupal_set_message(format_plural($created, 'Created @number term in !vocabulary.', 'Created @number terms in !vocabulary.', array('@number' => $created, '!vocabulary' => $vocabulary->name)));
-    }
-    elseif ($updated) {
-      drupal_set_message(format_plural($updated, 'Updated @number term in !vocabulary.', 'Updated @number terms in !vocabulary.', array('@number' => $updated, '!vocabulary' => $vocabulary->name)));
-    }
-    else {
-      drupal_set_message(t('There are no new terms.'));
+  /**
+   * Validates a term.
+   */
+  protected function entityValidate($term) {
+    if (empty($term->name)) {
+      throw new FeedsValidationException(t('Term name missing.'));
     }
   }
 
   /**
-   * Implementation of FeedsProcessor::clear().
+   * Saves a term.
    */
-  public function clear(FeedsBatch $batch, FeedsSource $source) {
-    $deleted = 0;
-    $vocabulary = $this->vocabulary();
-    $result = db_query("SELECT td.tid
-                        FROM {term_data} td
-                        JOIN {feeds_term_item} ft ON td.tid = ft.tid
-                        WHERE td.vid = %d
-                        AND ft.id = '%s'
-                        AND ft.feed_nid = %d",
-                        $vocabulary->vid, $this->id, $source->feed_nid);
-    while ($term = db_fetch_object($result)) {
-      if (taxonomy_del_term($term->tid) == SAVED_DELETED) {
-        $deleted++;
-      }
-    }
-    // Set messages.
-    if ($deleted) {
-      drupal_set_message(format_plural($deleted, 'Deleted @number term from !vocabulary.', 'Deleted @number terms from !vocabulary.', array('@number' => $deleted, '!vocabulary' => $vocabulary->name)));
-    }
-    else {
-      drupal_set_message(t('No terms to be deleted.'));
-    }
+  protected function entitySave($term) {
+    $term = (array) $term;
+    taxonomy_save_term($term);
   }
 
   /**
-   * Execute mapping on an item.
+   * Deletes a series of terms.
    */
-  protected function map(FeedsImportBatch $batch, $target_term = NULL) {
-    // Prepare term object, have parent class do the iterating.
-    if (!$target_term) {
-      $target_term = array();
-    }
-    if (!$vocabulary = $this->vocabulary()) {
-      throw new Exception(t('No vocabulary specified for term processor'));
+  protected function entityDeleteMultiple($tids) {
+    foreach ($tids as $tid) {
+      taxonomy_del_term($tid);
     }
-    $target_term['vid'] = $vocabulary->vid;
-    $target_term = parent::map($batch, $target_term);
-    // Taxonomy module expects synonyms to be supplied as a single string.
-    if (isset($target_term['synonyms']) && is_array($target_term['synonyms'])) {
-      $target_term['synonyms'] = implode("\n", $target_term['synonyms']);
-    }
-    return $target_term;
   }
 
   /**
@@ -136,9 +78,7 @@ class FeedsTermProcessor extends FeedsPr
   public function configDefaults() {
     return array(
       'vocabulary' => 0,
-      'update_existing' => FEEDS_SKIP_EXISTING,
-      'mappings' => array(),
-    );
+    ) + parent::configDefaults();
   }
 
   /**
@@ -146,15 +86,10 @@ class FeedsTermProcessor extends FeedsPr
    */
   public function configForm(&$form_state) {
     $options = array(0 => t('Select a vocabulary'));
-    foreach (taxonomy_get_vocabularies() as $vid => $vocab) {
-      if (strpos($vocab->module, 'features_') === 0) {
-        $options[$vocab->module] = $vocab->name;
-      }
-      else {
-        $options[$vid] = $vocab->name;
-      }
+    foreach (taxonomy_get_vocabularies() as $vocab) {
+      $options[$vocab->vid] = check_plain($vocab->name);
     }
-    $form = array();
+    $form = parent::configForm($form_state);
     $form['vocabulary'] = array(
       '#type' => 'select',
       '#title' => t('Import to vocabulary'),
@@ -162,17 +97,6 @@ class FeedsTermProcessor extends FeedsPr
       '#options' => $options,
       '#default_value' => $this->config['vocabulary'],
     );
-    $form['update_existing'] = array(
-      '#type' => 'radios',
-      '#title' => t('Update existing terms'),
-      '#description' => t('Select how existing terms should be updated. Existing terms will be determined using mappings that are a "unique target".'),
-      '#options' => array(
-        FEEDS_SKIP_EXISTING => 'Do not update existing terms',
-        FEEDS_REPLACE_EXISTING => 'Replace existing terms',
-        FEEDS_UPDATE_EXISTING => 'Update existing terms (slower than replacing them)',
-      ),
-      '#default_value' => $this->config['update_existing'],
-    );
     return $form;
   }
 
@@ -189,7 +113,8 @@ class FeedsTermProcessor extends FeedsPr
    * Return available mapping targets.
    */
   public function getMappingTargets() {
-    $targets = array(
+    $targets = parent::getMappingTargets();
+    $targets += array(
       'name' => array(
         'name' => t('Term name'),
         'description' => t('Name of the taxonomy term.'),
@@ -199,27 +124,31 @@ class FeedsTermProcessor extends FeedsPr
         'name' => t('Term description'),
         'description' => t('Description of the taxonomy term.'),
        ),
-      'synonyms' => array(
-        'name' => t('Term synonyms'),
-        'description' => t('One synonym or an array of synonyms of the taxonomy term.'),
-       ),
     );
     // Let implementers of hook_feeds_term_processor_targets() add their targets.
-    $vocabulary = $this->vocabulary();
-    drupal_alter('feeds_term_processor_targets', $targets, $vocabulary->vid);
+    try {
+      self::loadMappers();
+      feeds_alter('feeds_processor_targets', $targets, 'taxonomy_term', $this->vocabulary()->vid);
+    }
+    catch (Exception $e) {}
     return $targets;
   }
 
   /**
    * Get id of an existing feed item term if available.
    */
-  protected function existingItemId(FeedsImportBatch $batch, FeedsSource $source) {
+  protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
+    if ($tid = parent::existingEntityId($source, $result)) {
+      return $tid;
+    }
 
     // The only possible unique target is name.
-    foreach ($this->uniqueTargets($batch) as $target => $value) {
+    foreach ($this->uniqueTargets($source, $result) as $target => $value) {
       if ($target == 'name') {
         $vocabulary = $this->vocabulary();
-        if ($tid = db_result(db_query("SELECT tid FROM {term_data} WHERE name = '%s' AND vid = %d", $value, $vocabulary->vid))) {
+        if ($tid = db_result(db_query("SELECT tid FROM {term_data}
+                                      WHERE name = '%s' AND vid = %d",
+                                      $value, $vocabulary->vid))) {
           return $tid;
         }
       }
@@ -229,14 +158,10 @@ class FeedsTermProcessor extends FeedsPr
 
   /**
    * Return vocabulary to map to.
-   *
-   * Feeds supports looking up vocabularies by their module name as part of an
-   * effort to use the vocabulary.module field as machine name to make
-   * vocabularies exportable.
    */
   public function vocabulary() {
     $vocabularies = taxonomy_get_vocabularies();
-    if (is_numeric($this->config['vocabulary'])) {
+    if (!empty($this->config['vocabulary']) && is_numeric($this->config['vocabulary'])) {
       return $vocabularies[$this->config['vocabulary']];
     }
     else {
@@ -246,5 +171,6 @@ class FeedsTermProcessor extends FeedsPr
         }
       }
     }
+    throw new Exception(t('No vocabulary defined for Taxonomy Term processor.'));
   }
 }
Index: plugins/FeedsUserProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsUserProcessor.inc,v
retrieving revision 1.19
diff -u -p -r1.19 FeedsUserProcessor.inc
--- plugins/FeedsUserProcessor.inc	7 Sep 2010 17:29:36 -0000	1.19
+++ plugins/FeedsUserProcessor.inc	3 Feb 2011 13:04:03 -0000
@@ -10,99 +10,80 @@
  * Feeds processor plugin. Create users from feed items.
  */
 class FeedsUserProcessor extends FeedsProcessor {
-
   /**
-   * Implementation of FeedsProcessor::process().
+   * Define entity type.
    */
-  public function process(FeedsImportBatch $batch, FeedsSource $source) {
+  public function entityType() {
+    return 'user';
+  }
 
-    // Count number of created and updated nodes.
-    $created  = $updated = $failed = 0;
+  /**
+   * Implementation of parent::entityInfo().
+   */
+  protected function entityInfo() {
+    //$info = parent::entityInfo();
+    $info = array();
+    $info['label'] = t('User');
+    $info['label plural'] = t('Users');
+    $info['base table'] = 'users';
+    $info['entity keys']['id'] = 'uid';
+    return $info;
+  }
 
-    while ($item = $batch->shiftItem()) {
+  /**
+   * Creates a new user account in memory and returns it.
+   */
+  protected function newEntity(FeedsSource $source) {
+    $account = new stdClass();
+    $account->uid = 0;
+    $account->roles = array_filter($this->config['roles']);
+    $account->status = $this->config['status'];
+    return $account;
+  }
 
-      if (!($uid = $this->existingItemId($batch, $source)) || $this->config['update_existing']) {
-
-        // Map item to a term.
-        $account = $this->map($batch);
-
-        // Check if user name and mail are set, otherwise continue.
-        if (empty($account->name) || empty($account->mail) || !valid_email_address($account->mail)) {
-          $failed++;
-          continue;
-        }
-
-        // Add term id if available.
-        if (!empty($uid)) {
-          $account->uid = $uid;
-        }
-
-        // Save the user.
-        user_save($account, (array) $account);
-        if ($account->uid && $account->openid) {
-          $authmap = array(
-            'uid' => $account->uid,
-            'module' => 'openid',
-            'authname' => $account->openid,
-          );
-          if (SAVED_UPDATED != drupal_write_record('authmap', $authmap, array('uid', 'module'))) {
-            drupal_write_record('authmap', $authmap);
-          }
-        }
-
-        if ($uid) {
-          $updated++;
-        }
-        else {
-          $created++;
-        }
-      }
-    }
+  /**
+   * Loads an existing user.
+   */
+  protected function entityLoad(FeedsSource $source, $uid) {
+    return user_load($uid);
+  }
 
-    // Set messages.
-    if ($failed) {
-      drupal_set_message(
-        format_plural(
-          $failed,
-          'There was @number user that could not be imported because either their name or their email was empty or not valid. Check import data and mapping settings on User processor.',
-          'There were @number users that could not be imported because either their name or their email was empty or not valid. Check import data and mapping settings on User processor.',
-          array('@number' => $failed)
-        ),
-        'error'
-      );
-    }
-    if ($created) {
-      drupal_set_message(format_plural($created, 'Created @number user.', 'Created @number users.', array('@number' => $created)));
-    }
-    elseif ($updated) {
-      drupal_set_message(format_plural($updated, 'Updated @number user.', 'Updated @number users.', array('@number' => $updated)));
-    }
-    else {
-      drupal_set_message(t('There are no new users.'));
+  /**
+   * Validates a user account.
+   */
+  protected function entityValidate($account) {
+    if (empty($account->name) || empty($account->mail) || !valid_email_address($account->mail)) {
+      throw new FeedsValidationException(t('User name missing or email not valid.'));
     }
   }
 
   /**
-   * Implementation of FeedsProcessor::clear().
+   * Save a user account.
    */
-  public function clear(FeedsBatch $batch, FeedsSource $source) {
-    // Do not support deleting users as we have no way of knowing which ones we
-    // imported.
-    throw new Exception(t('User processor does not support deleting users.'));
+  protected function entitySave($account) {
+    if ($this->config['defuse_mail']) {
+      $account->mail = $account->mail . '_test';
+    }
+    user_save($account, (array) $account);
+    if ($account->uid && $account->openid) {
+      $authmap = array(
+        'uid' => $account->uid,
+        'module' => 'openid',
+        'authname' => $account->openid,
+      );
+      if (SAVED_UPDATED != drupal_write_record('authmap', $authmap, array('uid', 'module'))) {
+        drupal_write_record('authmap', $authmap);
+      }
+    }
   }
 
   /**
-   * Execute mapping on an item.
+   * Delete multiple user accounts.
    */
-  protected function map(FeedsImportBatch $batch) {
-    // Prepare term object.
-    $target_account = new stdClass();
-    $target_account->uid = 0;
-    $target_account->roles = array_filter($this->config['roles']);
-    $target_account->status = $this->config['status'];
-
-    // Have parent class do the iterating.
-    return parent::map($batch, $target_account);
+  protected function entityDeleteMultiple($uids) {
+    foreach ($uids as $uid) {
+      user_delete($uid);
+    }
   }
 
   /**
@@ -111,18 +92,16 @@ class FeedsUserProcessor extends FeedsPr
   public function configDefaults() {
     return array(
       'roles' => array(),
-      'update_existing' => FALSE,
       'status' => 1,
-      'mappings' => array(),
-    );
+      'defuse_mail' => FALSE,
+    ) + parent::configDefaults();
   }
 
   /**
    * Override parent::configForm().
    */
   public function configForm(&$form_state) {
-    $form = array();
-
+    $form = parent::configForm($form_state);
     $form['status'] = array(
       '#type' => 'radios',
       '#title' => t('Status'),
@@ -149,21 +128,35 @@ class FeedsUserProcessor extends FeedsPr
       '#description' => t('If an existing user is found for an imported user, replace it. Existing users will be determined using mappings that are a "unique target".'),
       '#default_value' => $this->config['update_existing'],
     );
+    $form['defuse_mail'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Defuse e-mail addresses'),
+      '#description' => t('This appends _test to all imported e-mail addresses to ensure they cannot be used as recipients.'),
+      '#default_value' => $this->config['defuse_mail'],
+    );
     return $form;
   }
 
   /**
-   * Set target element.
+   * Override setTargetElement to operate on a target item that is a node.
    */
-  public function setTargetElement(&$target_item, $target_element, $value) {
-    $target_item->$target_element = $value;
+  public function setTargetElement(FeedsSource $source, $target_user, $target_element, $value) {
+    switch ($target_element) {
+      case 'created':
+        $target_user->created = feeds_to_unixtime($value, FEEDS_REQUEST_TIME);
+        break;
+      default:
+        parent::setTargetElement($source, $target_user, $target_element, $value);
+        break;
+    }
   }
 
   /**
    * Return available mapping targets.
    */
   public function getMappingTargets() {
-    $targets = array(
+    $targets = parent::getMappingTargets();
+    $targets += array(
       'name' => array(
         'name' => t('User name'),
         'description' => t('Name of the user.'),
@@ -189,7 +182,7 @@ class FeedsUserProcessor extends FeedsPr
 
     // Let other modules expose mapping targets.
     self::loadMappers();
-    drupal_alter('feeds_user_processor_targets', $targets);
+    feeds_alter('feeds_processor_targets', $targets, 'user', 'user');
 
     return $targets;
   }
@@ -197,11 +190,14 @@ class FeedsUserProcessor extends FeedsPr
   /**
    * Get id of an existing feed item term if available.
    */
-  protected function existingItemId(FeedsImportBatch $batch, FeedsSource $source) {
+  protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
+    if ($uid = parent::existingEntityId($source, $result)) {
+      return $uid;
+    }
 
     // Iterate through all unique targets and try to find a user for the
     // target's value.
-    foreach ($this->uniqueTargets($batch) as $target => $value) {
+    foreach ($this->uniqueTargets($source, $result) as $target => $value) {
       switch ($target) {
         case 'name':
           $uid = db_result(db_query("SELECT uid FROM {users} WHERE name = '%s'", $value));
Index: tests/feeds.test.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds.test.inc,v
retrieving revision 1.15
diff -u -p -r1.15 feeds.test.inc
--- tests/feeds.test.inc	15 Sep 2010 19:27:42 -0000	1.15
+++ tests/feeds.test.inc	3 Feb 2011 13:04:03 -0000
@@ -110,7 +110,10 @@ class FeedsWebTestCase extends DrupalWeb
    *   feeds_feeds_plugins()).
    */
   public function setPlugin($id, $plugin_key) {
-    if ($type = feeds_plugin_type($plugin_key)) {
+    if (!class_exists('FeedsPlugin')) {
+      feeds_include('FeedsPlugin', 'plugins');
+    }
+    if ($type = FeedsPlugin::typeOf($plugin_key)) {
       $edit = array(
         'plugin_key' => $plugin_key,
       );
Index: tests/feeds_date_time.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/Attic/feeds_date_time.test,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 feeds_date_time.test
--- tests/feeds_date_time.test	25 Sep 2010 16:47:01 -0000	1.1.2.1
+++ tests/feeds_date_time.test	3 Feb 2011 13:04:03 -0000
@@ -32,7 +32,8 @@ class FeedsDateTimeTest extends DrupalWe
     parent::setUp('feeds', 'feeds_ui', 'ctools', 'job_scheduler');
     // Trick feeds into loading the FeedsParser class file.
     // @todo: break out FeedsElement and children into its own include file.
-    feeds_plugin_instance('FeedsCSVParser', 'test');
+    feeds_include('FeedsPlugin', 'plugins');
+    feeds_include('FeedsParser', 'plugins');
   }
 
   /**
Index: tests/feeds_mapper_content.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_mapper_content.test,v
retrieving revision 1.5
diff -u -p -r1.5 feeds_mapper_content.test
--- tests/feeds_mapper_content.test	15 Sep 2010 19:27:42 -0000	1.5
+++ tests/feeds_mapper_content.test	3 Feb 2011 13:04:03 -0000
@@ -91,7 +91,7 @@ class FeedsMapperContentTestCase extends
 
     // Import CSV file.
     $this->importFile('csv', $this->absolutePath() .'/tests/feeds/content.csv');
-    $this->assertText('Created 2 '. $typename .' nodes.');
+    $this->assertText('Created 2 nodes.');
 
     // Check the two imported files.
     $this->drupalGet('node/1/edit');
Index: tests/feeds_mapper_date.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_mapper_date.test,v
retrieving revision 1.5
diff -u -p -r1.5 feeds_mapper_date.test
--- tests/feeds_mapper_date.test	15 Sep 2010 19:27:42 -0000	1.5
+++ tests/feeds_mapper_date.test	3 Feb 2011 13:04:03 -0000
@@ -80,7 +80,7 @@ class FeedsMapperDateTestCase extends Fe
 
     // Import CSV file.
     $this->importFile('daterss', $this->absolutePath() .'/tests/feeds/googlenewstz.rss2');
-    $this->assertText('Created 6 '. $typename .' nodes.');
+    $this->assertText('Created 6 nodes.');
 
     // Check the imported nodes.
     $values = array(
Index: tests/feeds_mapper_email.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/Attic/feeds_mapper_email.test,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 feeds_mapper_email.test
--- tests/feeds_mapper_email.test	16 Nov 2010 23:02:18 -0000	1.1.2.1
+++ tests/feeds_mapper_email.test	3 Feb 2011 13:04:03 -0000
@@ -76,7 +76,7 @@ class FeedsMapperEmailTestCase extends F
 
     // Import CSV file.
     $this->importFile('csv', $this->absolutePath() .'/tests/feeds/email.csv');
-    $this->assertText('Created 2 '. $typename .' nodes.');
+    $this->assertText('Created 2 nodes.');
 
     // Check the two imported files.
     $this->drupalGet('node/1/edit');
Index: tests/feeds_mapper_filefield.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_mapper_filefield.test,v
retrieving revision 1.9
diff -u -p -r1.9 feeds_mapper_filefield.test
--- tests/feeds_mapper_filefield.test	15 Sep 2010 19:27:42 -0000	1.9
+++ tests/feeds_mapper_filefield.test	3 Feb 2011 13:04:03 -0000
@@ -83,7 +83,7 @@ class FeedsMapperFileFieldTestCase exten
     ));
 
     $nid = $this->createFeedNode('syndication', $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/flickr.xml');
-    $this->assertText('Created 4 '. $typename .' nodes.');
+    $this->assertText('Created 4 nodes.');
 
     $filename = array('3596408735_ce2f0c4824_b', '2640019371_495c3f51a2_b', '3686290986_334c427e8c_b', '2640845934_85c11e5a18_b');
     for($i = 0; $i < 4; $i++) {
Index: tests/feeds_mapper_link.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_mapper_link.test,v
retrieving revision 1.5
diff -u -p -r1.5 feeds_mapper_link.test
--- tests/feeds_mapper_link.test	15 Sep 2010 19:27:42 -0000	1.5
+++ tests/feeds_mapper_link.test	3 Feb 2011 13:04:03 -0000
@@ -125,7 +125,7 @@ class FeedsMapperLinkTestCase extends Fe
     // Import RSS file.
     $nid = $this->createFeedNode();
     // Assert 10 items aggregated after creation of the node.
-    $this->assertText('Created 10 '. $typename .' nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // Edit the imported node.
     $this->drupalGet('node/2/edit');
Index: tests/feeds_mapper_locale.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_mapper_locale.test,v
retrieving revision 1.4
diff -u -p -r1.4 feeds_mapper_locale.test
--- tests/feeds_mapper_locale.test	15 Sep 2010 19:27:42 -0000	1.4
+++ tests/feeds_mapper_locale.test	3 Feb 2011 13:04:03 -0000
@@ -110,7 +110,7 @@ class FeedsMapperLocaleTestCase extends 
     );
     $this->drupalPost("node/$nid/edit", $edit, t('Save'));
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE language = 'zh-hans'", $nid));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE language = 'zh-hans'"));
     $this->assertEqual(11, $count, 'Found correct number of nodes.');
   }
 }
Index: tests/feeds_mapper_taxonomy.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_mapper_taxonomy.test,v
retrieving revision 1.6
diff -u -p -r1.6 feeds_mapper_taxonomy.test
--- tests/feeds_mapper_taxonomy.test	15 Sep 2010 19:27:42 -0000	1.6
+++ tests/feeds_mapper_taxonomy.test	3 Feb 2011 13:04:04 -0000
@@ -130,7 +130,7 @@ class FeedsMapperTaxonomyTestCase extend
     // Aggregate feed node with "Tag" vocabulary.
     $nid = $this->createFeedNode();
     // Assert 10 items aggregated after creation of the node.
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
     // There should be 30 terms and 44 term-node relations.
     $this->assertEqual(30, db_result(db_query("SELECT count(*) FROM {term_data}")), "Found correct number of terms.");
     $this->assertEqual(44, db_result(db_query("SELECT count(*) FROM {term_node}")), "Found correct number of term-node relations.");
@@ -185,7 +185,7 @@ class FeedsMapperTaxonomyTestCase extend
     );
     $this->drupalPost('admin/content/taxonomy/edit/vocabulary/1', $edit, 'Save');
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // We should only get one term-node association per node.
     $this->assertEqual(30, db_result(db_query("SELECT count(*) FROM {term_data}")), "Found correct number of terms.");
@@ -200,7 +200,7 @@ class FeedsMapperTaxonomyTestCase extend
     );
     $this->drupalPost('admin/content/taxonomy/edit/vocabulary/1', $edit, 'Save');
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // We should get all term-node associations again.
     $this->assertEqual(30, db_result(db_query("SELECT count(*) FROM {term_data}")), "Found correct number of terms.");
@@ -214,7 +214,7 @@ class FeedsMapperTaxonomyTestCase extend
     $this->drupalPost(NULL, array(), 'Delete');
     $this->assertText('Deleted term');
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // This term should now be missing from term-node associations.
     $this->assertEqual(29, db_result(db_query("SELECT count(*) FROM {term_data}")), "Found correct number of terms.");
Index: tests/feeds_parser_sitemap.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_parser_sitemap.test,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_parser_sitemap.test
--- tests/feeds_parser_sitemap.test	15 Sep 2010 19:27:42 -0000	1.2
+++ tests/feeds_parser_sitemap.test	3 Feb 2011 13:04:04 -0000
@@ -80,63 +80,68 @@ class FeedsSitemapParserTestCase extends
 
     $path = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/';
     $nid = $this->createFeedNode('sitemap', $path .'sitemap-example.xml', 'Testing Sitemap Parser');
-    $this->assertText('Created 5 Story nodes.');
+    $this->assertText('Created 5 nodes');
 
     // Assert DB status.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node'"));
     $this->assertEqual($count, 5, 'Accurate number of items in database.');
 
     // Check items against known content of feed.
-    $result = db_query('SELECT * FROM {feeds_node_item} WHERE feed_nid = %d ORDER BY nid', $nid);
+    $items = db_query("SELECT * FROM {feeds_item} WHERE entity_type = 'node' AND feed_nid = %d ORDER BY entity_id", $nid);
 
     // Check first item.
     date_default_timezone_set('GMT');
-    $item = db_fetch_object($result);
-    $node = node_load($item->nid);
+    $item = db_fetch_object($items);
+    $node = node_load($item->entity_id);
     $this->assertEqual($node->title, 'monthly', 'Feed item 1 changefreq is correct.');
     $this->assertEqual($node->body, '0.8', 'Feed item 1 priority is correct.');
     $this->assertEqual($node->created, strtotime('2005-01-01'), 'Feed item 1 lastmod is correct.');
-    $this->assertEqual($node->feeds_node_item->url, 'http://www.example.com/', 'Feed item 1 url is correct.');
-    $this->assertEqual($node->feeds_node_item->url, $node->feeds_node_item->guid, 'Feed item 1 guid is correct.');
+    $info = feeds_item_info_load('node', $node->nid);
+    $this->assertEqual($info->url, 'http://www.example.com/', 'Feed item 1 url is correct.');
+    $this->assertEqual($info->url, $info->guid, 'Feed item 1 guid is correct.');
 
     // Check second item.
-    $item = db_fetch_object($result);
-    $node = node_load($item->nid);
+    $item = db_fetch_object($items);
+    $node = node_load($item->entity_id);
     $this->assertEqual($node->title, 'weekly', 'Feed item 2 changefreq is correct.');
     $this->assertEqual($node->body, '', 'Feed item 2 priority is correct.');
     // $node->created is... recently
-    $this->assertEqual($node->feeds_node_item->url, 'http://www.example.com/catalog?item=12&desc=vacation_hawaii', 'Feed item 2 url is correct.');
-    $this->assertEqual($node->feeds_node_item->url, $node->feeds_node_item->guid, 'Feed item 2 guid is correct.');
+    $info = feeds_item_info_load('node', $node->nid);
+    $this->assertEqual($info->url, 'http://www.example.com/catalog?item=12&desc=vacation_hawaii', 'Feed item 2 url is correct.');
+    $this->assertEqual($info->url, $info->guid, 'Feed item 2 guid is correct.');
 
     // Check third item.
-    $item = db_fetch_object($result);
-    $node = node_load($item->nid);
+    $item = db_fetch_object($items);
+    $node = node_load($item->entity_id);
     $this->assertEqual($node->title, 'weekly', 'Feed item 3 changefreq is correct.');
     $this->assertEqual($node->body, '', 'Feed item 3 priority is correct.');
     $this->assertEqual($node->created, strtotime('2004-12-23'), 'Feed item 3 lastmod is correct.');
-    $this->assertEqual($node->feeds_node_item->url, 'http://www.example.com/catalog?item=73&desc=vacation_new_zealand', 'Feed item 3 url is correct.');
-    $this->assertEqual($node->feeds_node_item->url, $node->feeds_node_item->guid, 'Feed item 3 guid is correct.');
+    $info = feeds_item_info_load('node', $node->nid);
+    $this->assertEqual($info->url, 'http://www.example.com/catalog?item=73&desc=vacation_new_zealand', 'Feed item 3 url is correct.');
+    $this->assertEqual($info->url, $info->guid, 'Feed item 3 guid is correct.');
 
     // Check fourth item.
-    $item = db_fetch_object($result);
-    $node = node_load($item->nid);
+    $item = db_fetch_object($items);
+    $node = node_load($item->entity_id);
     $this->assertEqual($node->title, '', 'Feed item 4 changefreq is correct.');
     $this->assertEqual($node->body, '0.3', 'Feed item 4 priority is correct.');
     $this->assertEqual($node->created, strtotime('2004-12-23T18:00:15+00:00'), 'Feed item 4 lastmod is correct.');
-    $this->assertEqual($node->feeds_node_item->url, 'http://www.example.com/catalog?item=74&desc=vacation_newfoundland', 'Feed item 4 url is correct.');
-    $this->assertEqual($node->feeds_node_item->url, $node->feeds_node_item->guid, 'Feed item 1 guid is correct.');
+    $info = feeds_item_info_load('node', $node->nid);
+    $this->assertEqual($info->url, 'http://www.example.com/catalog?item=74&desc=vacation_newfoundland', 'Feed item 4 url is correct.');
+    $this->assertEqual($info->url, $info->guid, 'Feed item 1 guid is correct.');
 
     // Check fifth item.
-    $item = db_fetch_object($result);
-    $node = node_load($item->nid);
+    $item = db_fetch_object($items);
+    $node = node_load($item->entity_id);
     $this->assertEqual($node->title, '', 'Feed item 5 changefreq is correct.');
     $this->assertEqual($node->body, '', 'Feed item 5 priority is correct.');
     $this->assertEqual($node->created, strtotime('2004-11-23'), 'Feed item 5 lastmod is correct.');
-    $this->assertEqual($node->feeds_node_item->url, 'http://www.example.com/catalog?item=83&desc=vacation_usa', 'Feed item 5 url is correct.');
-    $this->assertEqual($node->feeds_node_item->url, $node->feeds_node_item->guid, 'Feed item 5 guid is correct.');
+    $info = feeds_item_info_load('node', $node->nid);
+    $this->assertEqual($info->url, 'http://www.example.com/catalog?item=83&desc=vacation_usa', 'Feed item 5 url is correct.');
+    $this->assertEqual($info->url, $info->guid, 'Feed item 5 guid is correct.');
 
     // Check for more items.
-    $item = db_fetch_object($result);
+    $item = db_fetch_object($items);
     $this->assertFalse($item, 'Correct number of feed items recorded.');
   }
 }
Index: tests/feeds_parser_syndication.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_parser_syndication.test,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_parser_syndication.test
--- tests/feeds_parser_syndication.test	15 Sep 2010 19:27:42 -0000	1.2
+++ tests/feeds_parser_syndication.test	3 Feb 2011 13:04:04 -0000
@@ -50,7 +50,7 @@ class FeedsSyndicationParserTestCase ext
       $this->setPlugin('syndication', $parser);
       foreach ($this->feedUrls() as $url => $assertions) {
         $this->createFeedNode('syndication', $url);
-        $this->assertText('Created '. $assertions['item_count'] .' Story nodes.');
+        $this->assertText('Created '. $assertions['item_count'] .' nodes.');
       }
     }
   }
Index: tests/feeds_processor_node.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_processor_node.test,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_processor_node.test
--- tests/feeds_processor_node.test	15 Sep 2010 19:27:42 -0000	1.2
+++ tests/feeds_processor_node.test	3 Feb 2011 13:04:05 -0000
@@ -84,7 +84,7 @@ class FeedsRSStoNodesTest extends FeedsW
 
     $nid = $this->createFeedNode();
     // Assert 10 items aggregated after creation of the node.
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // Navigate to feed node, there should be Feeds tabs visible.
     $this->drupalGet('node/'. $nid);
@@ -96,7 +96,7 @@ class FeedsRSStoNodesTest extends FeedsW
     $this->drupalGet('node/'. $story_nid);
     $this->assertNoRaw('node/'. $story_nid .'/import');
     $this->assertNoRaw('node/'. $story_nid .'/delete-items');
-    $this->assertEqual("Created/updated by FeedsNodeProcessor", db_result(db_query("SELECT nr.log FROM {node} n JOIN {node_revisions} nr ON n.vid = nr.vid WHERE n.nid = %d", $story_nid)));
+    $this->assertEqual("Created by FeedsNodeProcessor", db_result(db_query("SELECT nr.log FROM {node} n JOIN {node_revisions} nr ON n.vid = nr.vid WHERE n.nid = %d", $story_nid)));
 
     // Assert accuracy of aggregated information.
     $this->drupalGet('node');
@@ -133,24 +133,24 @@ class FeedsRSStoNodesTest extends FeedsW
     $this->assertText('The first major change is switching');
 
     // Assert DB status.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_node_item} fn ON n.nid = fn.nid"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_item} fi ON n.nid = fi.entity_id"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Assert default input format on first imported feed node.
-    $format = db_result(db_query_range("SELECT nr.format FROM {feeds_node_item} fi JOIN {node} n ON fi.nid = n.nid JOIN {node_revisions} nr ON n.vid = nr.vid", 0, 1));
+    $format = db_result(db_query_range("SELECT nr.format FROM {feeds_item} fi JOIN {node} n ON fi.entity_id = n.nid JOIN {node_revisions} nr ON n.vid = nr.vid", 0, 1));
     $this->assertEqual($format, FILTER_FORMAT_DEFAULT, 'Using default Input format.');
 
     // Import again.
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('There is no new content.');
+    $this->assertText('There are no new nodes.');
 
     // Assert DB status, there still shouldn't be more than 10 items.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_node_item} fn ON n.nid = fn.nid"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_item} fi ON n.nid = fi.entity_id"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // All of the above tests should have produced published nodes, set default
     // to unpublished, import again.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_node_item} fn ON n.nid = fn.nid WHERE n.status = 1"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_item} fi ON n.nid = fi.entity_id WHERE n.status = 1"));
     $this->assertEqual($count, 10, 'All items are published.');
     $edit = array(
       'node_options[status]' => FALSE,
@@ -158,7 +158,7 @@ class FeedsRSStoNodesTest extends FeedsW
     $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type'));
     $this->drupalPost('node/'. $nid .'/delete-items', array(), 'Delete');
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_node_item} fn ON n.nid = fn.nid WHERE n.status = 0"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {feeds_item} fi ON n.nid = fi.entity_id WHERE n.status = 0"));
     $this->assertEqual($count, 10, 'No items are published.');
     $edit = array(
       'node_options[status]' => TRUE,
@@ -172,7 +172,7 @@ class FeedsRSStoNodesTest extends FeedsW
     $feed_url = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/developmentseed_changes.rss2';
     $this->editFeedNode($nid, $feed_url);
     $this->drupalPost('node/' . $nid . '/import', array(), 'Import');
-    $this->assertText('Updated 2 Story nodes.');
+    $this->assertText('Updated 2 nodes.');
 
     // Assert accuracy of aggregated content (check 2 updates, one original).
     $this->drupalGet('node');
@@ -182,10 +182,10 @@ class FeedsRSStoNodesTest extends FeedsW
 
     // Import again.
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('There is no new content.');
+    $this->assertText('There are no new nodes.');
 
     // Assert DB status, there still shouldn't be more than 10 items.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Now delete all items.
@@ -193,7 +193,7 @@ class FeedsRSStoNodesTest extends FeedsW
     $this->assertText('Deleted 10 nodes.');
 
     // Assert DB status, now there should be no items.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 0, 'Accurate number of items in database.');
 
     // Change author.
@@ -205,25 +205,25 @@ class FeedsRSStoNodesTest extends FeedsW
 
     // Import again.
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // Assert author.
     $this->drupalGet('node');
     $this->assertPattern('/<span class="submitted">(.*?)'. check_plain($author->name) .'<\/span>/');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item} fi JOIN {node} n ON fi.nid = n.nid WHERE n.uid = %d", $author->uid));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} fi JOIN {node} n ON fi.entity_id = n.nid WHERE n.uid = %d", $author->uid));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Assert input format.
-    $format = db_result(db_query_range("SELECT nr.format FROM {feeds_node_item} fi JOIN {node} n ON fi.nid = n.nid JOIN {node_revisions} nr ON n.vid = nr.vid", 0, 1));
+    $format = db_result(db_query_range("SELECT nr.format FROM {feeds_item} fi JOIN {node} n ON fi.entity_id = n.nid JOIN {node_revisions} nr ON n.vid = nr.vid", 0, 1));
     $this->assertEqual($format, FILTER_FORMAT_DEFAULT + 1, 'Set non-default Input format.');
 
     // Set to update existing, remove authorship of above nodes and import again.
     $this->setSettings('syndication', 'FeedsNodeProcessor', array('update_existing' => 2));
-    db_query("UPDATE {node} n JOIN {feeds_node_item} fi ON n.nid = fi.nid SET n.uid = 0, fi.hash=''");
+    db_query("UPDATE {node} n JOIN {feeds_item} fi ON n.nid = fi.entity_id SET n.uid = 0, fi.hash=''");
     $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
     $this->drupalGet('node');
     $this->assertNoPattern('/<span class="submitted">(.*?)'. check_plain($author->name) .'<\/span>/');
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item} fi JOIN {node} n ON fi.nid = n.nid WHERE n.uid = %d", $author->uid));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item} fi JOIN {node} n ON fi.entity_id = n.nid WHERE n.uid = %d", $author->uid));
     $this->assertEqual($count, 0, 'Accurate number of items in database.');
 
     // Map feed node's author to feed item author, update - feed node's items
@@ -300,7 +300,7 @@ class FeedsRSStoNodesTest extends FeedsW
 
     // Import, assert 10 items aggregated after creation of the node.
     $this->importURL('syndication_standalone');
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // Assert accuracy of aggregated information.
     $this->drupalGet('node');
@@ -336,22 +336,22 @@ class FeedsRSStoNodesTest extends FeedsW
     $this->assertText('The first major change is switching');
 
     // Assert DB status.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Import again.
     $this->drupalPost('import/syndication_standalone', array(), 'Import');
-    $this->assertText('There is no new content.');
+    $this->assertText('There are no new nodes.');
 
     // Assert DB status, there still shouldn't be more than 10 items.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Enable replace existing and import updated feed file.
     $this->setSettings('syndication_standalone', 'FeedsNodeProcessor', array('update_existing' => 1));
     $feed_url = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') . '/tests/feeds/developmentseed_changes.rss2';
     $this->importURL('syndication_standalone', $feed_url);
-    $this->assertText('Updated 2 Story nodes.');
+    $this->assertText('Updated 2 nodes.');
 
     // Assert accuracy of aggregated information (check 2 updates, one orig).
     $this->drupalGet('node');
@@ -361,10 +361,10 @@ class FeedsRSStoNodesTest extends FeedsW
 
     // Import again.
     $this->drupalPost('import/syndication_standalone', array(), 'Import');
-    $this->assertText('There is no new content.');
+    $this->assertText('There are no new nodes.');
 
     // Assert DB status, there still shouldn't be more than 10 items.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Now delete all items.
@@ -372,15 +372,15 @@ class FeedsRSStoNodesTest extends FeedsW
     $this->assertText('Deleted 10 nodes.');
 
     // Assert DB status, now there should be no items.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 0, 'Accurate number of items in database.');
 
     // Import again, we should find new content.
     $this->drupalPost('import/syndication_standalone', array(), 'Import');
-    $this->assertText('Created 10 Story nodes.');
+    $this->assertText('Created 10 nodes.');
 
     // Assert DB status, there should be 10 again.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_node_item}"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {feeds_item}"));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Login with new user with only access content permissions.
@@ -402,6 +402,6 @@ class FeedsRSStoNodesTest extends FeedsW
     );
     $this->drupalPost('node/add/page', $edit, 'Save');
     $this->assertText('has been created.');
-    $this->assertText('Created 25 Story nodes.');
+    $this->assertText('Created 25 nodes.');
   }
 }
Index: tests/feeds_processor_term.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_processor_term.test,v
retrieving revision 1.3
diff -u -p -r1.3 feeds_processor_term.test
--- tests/feeds_processor_term.test	15 Sep 2010 19:27:42 -0000	1.3
+++ tests/feeds_processor_term.test	3 Feb 2011 13:04:05 -0000
@@ -79,7 +79,8 @@ class FeedsCSVtoTermsTest extends FeedsW
 
     // Import and assert.
     $this->importFile('term_import', $this->absolutePath() .'/tests/feeds/users.csv');
-    $this->assertText('Created 5 terms in Addams vocabulary.');
+    $this->assertText('Created 5 terms.');
+    $this->assertText('5 imported items total.');
     $this->drupalGet('admin/content/taxonomy/1');
     $this->assertText('Morticia');
     $this->assertText('Fester');
@@ -96,6 +97,7 @@ class FeedsCSVtoTermsTest extends FeedsW
     );
     $this->drupalPost('admin/content/taxonomy/1/add/term', $edit, t('Save'));
     $this->drupalPost('import/term_import/delete-items', array(), t('Delete'));
+    $this->drupalGet('import/term_import');
     $this->drupalGet('admin/content/taxonomy/1');
     $this->assertText('Cousin Itt');
     $this->assertNoText('Morticia');
Index: tests/feeds_processor_user.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_processor_user.test,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_processor_user.test
--- tests/feeds_processor_user.test	15 Sep 2010 19:27:42 -0000	1.2
+++ tests/feeds_processor_user.test	3 Feb 2011 13:04:06 -0000
@@ -95,7 +95,7 @@ class FeedsCSVtoUsersTest extends FeedsW
     $this->assertText('Created 4 users.');
     // 1 user has an invalid email address, all users should be assigned
     // the manager role.
-    $this->assertText('There was 1 user that could not be imported because either their name or their email was empty or not valid. Check import data and mapping settings on User processor.');
+    $this->assertText('Failed importing 1 user.');
     $this->drupalGet('admin/user/user');
     $this->assertText('Morticia');
     $this->assertText('Fester');
Index: tests/feeds_scheduler.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds_scheduler.test,v
retrieving revision 1.3
diff -u -p -r1.3 feeds_scheduler.test
--- tests/feeds_scheduler.test	15 Sep 2010 19:27:42 -0000	1.3
+++ tests/feeds_scheduler.test	3 Feb 2011 13:04:06 -0000
@@ -6,9 +6,6 @@
  * Feeds tests.
  */
 
-// Require FeedsWebTestCase class definition.
-require_once(dirname(__FILE__) .'/feeds.test.inc');
-
 /**
  * Test cron scheduling.
  */
@@ -31,6 +28,13 @@ class FeedsSchedulerTestCase extends Fee
   public function setUp() {
     parent::setUp('feeds', 'feeds_ui', 'ctools', 'job_scheduler');
     $this->loginAdmin();
+  }
+
+  /**
+   * Test scheduling on cron.
+   */
+  public function testScheduling() {
+    // Create importer configuration.
     $this->createImporterConfiguration();
     $this->addMappings('syndication',
       array(
@@ -61,18 +65,16 @@ class FeedsSchedulerTestCase extends Fee
         ),
       )
     );
-  }
+    // This makes testing coincide with 7.
+    variable_set('job_schedule_num', 10);
+
 
-  /**
-   * Test scheduling on cron.
-   */
-  public function testScheduling() {
     // Create 10 feed nodes. Turn off import on create before doing that.
     $edit = array(
       'import_on_create' => FALSE,
     );
     $this->drupalPost('admin/build/feeds/edit/syndication/settings', $edit, 'Save');
-    $this->assertText('Do not import on create');
+    $this->assertText('Import on submission');
 
     $nids = $this->createFeedNodes();
     // This implicitly tests the import_on_create node setting being 0.
@@ -80,7 +82,13 @@ class FeedsSchedulerTestCase extends Fee
 
     // Check whether feed got properly added to scheduler.
     foreach ($nids as $nid) {
-      $this->assertEqual(1, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND id = %d AND callback = 'feeds_source_import' AND last <> 0 AND scheduled = 0 AND period = 1800 AND periodic = 1", $nid)));
+      $this->assertEqual(1, db_result(db_query("SELECT COUNT(*) FROM {job_schedule}
+                                     WHERE type = 'syndication' AND id = %d
+                                     AND callback = 'feeds_source_import'
+                                     AND last <> 0
+                                     AND scheduled = 0
+                                     AND period = 1800
+                                     AND periodic = 1", $nid)));
     }
 
     // Take time for comparisons.
@@ -89,73 +97,65 @@ class FeedsSchedulerTestCase extends Fee
 
     // Log out and run cron, no changes.
     $this->drupalLogout();
-    $this->runCron();
-    $count = db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE last > %d", $time));
+    $this->cronRun();
+    $count = db_result(db_query("SELECT COUNT(*) FROM {job_schedule}
+                                WHERE last > %d", $time));
     $this->assertEqual($count, 0, '0 feeds refreshed on cron.');
 
     // Set next time to 0 to simulate updates.
-    // There should be 2 x job_schedule_num (= 10) feeds updated now.
+    // There should be 2 x job_schedule_num (= 5) feeds updated now.
     db_query("UPDATE {job_schedule} SET next = 0");
-    $this->runCron();
-    $this->runCron();
-
-    $schedule = array();
-    $count = db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE last > %d", $time));
-    $this->assertEqual($count, 10, '10 feeds refreshed on cron.');
-    $result = db_query("SELECT * FROM {job_schedule}", $time);
-
-    // There should be 100 story nodes in the database.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story'"));
-    $this->assertEqual($count, 100, 'There are 100 story nodes aggregated.');
-
-    // Hit twice cron again.
-    $this->runCron();
-    $this->runCron();
+    $this->cronRun();
+    $this->cronRun();
 
     // There should be feeds_schedule_num X 2 (= 20) feeds updated now.
     $schedule = array();
-    $result = db_query("SELECT id, last, scheduled FROM {job_schedule} WHERE last > %d", $time);
-    while ($row = db_fetch_object($result)) {
+    $rows = db_query("SELECT id, last, scheduled FROM {job_schedule}
+                     WHERE last > %d", $time);
+    while ($row = db_fetch_object($rows)) {
       $schedule[$row->id] = $row;
     }
-    $this->assertEqual(count($schedule), 20, '20 feeds refreshed on cron.');
+    // Is cron really supposed to run 10 at a time it seems like 5 to me.
+    $this->assertEqual(count($schedule), 20, '20 feeds refreshed on cron.'. $count);
 
     // There should be 200 story nodes in the database.
     $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story' AND status = 1"));
-    $this->assertEqual($count, 200, 'There are 200 story nodes aggregated.');
+    $this->assertEqual($count, 200, 'There are 100 story nodes aggregated.'. $count);
 
     // There shouldn't be any items with scheduled = 1 now, if so, this would
     // mean they are stuck.
     $count = db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE scheduled = 1"));
-    $this->assertEqual($count, 0, 'All items are unscheduled (schedule flag = 0).');
+    $this->assertEqual($count, 0, 'All items are unscheduled (schedule flag = 0).'. $count);
 
     // Hit cron again twice.
-    $this->runCron();
-    $this->runCron();
+    $this->cronRun();
+    $this->cronRun();
 
     // The import_period setting of the feed configuration is 1800, there
     // shouldn't be any change to the database now.
     $equal = TRUE;
-    $result = db_query("SELECT id, last, scheduled FROM {job_schedule} WHERE last > %d", $time);
-    while ($row = db_fetch_object($result)) {
+    $rows = db_query("SELECT id, last, scheduled FROM {job_schedule}
+                     WHERE last > %d", $time);
+    while ($row = db_fetch_object($rows)) {
       $equal = $equal && ($row->last == $schedule[$row->id]->last);
     }
     $this->assertTrue($equal, 'Schedule did not change.');
 
     // Log back in and set refreshing to as often as possible.
-    $this->loginAdmin();
+    $this->drupalLogin($this->admin_user);
     $edit = array(
       'import_period' => 0,
     );
     $this->drupalPost('admin/build/feeds/edit/syndication/settings', $edit, 'Save');
-    $this->assertText('Refresh: as often as possible');
+    $this->assertText('as often as possible');
     $this->drupalLogout();
 
     // Hit cron once, this should cause Feeds to reschedule all entries.
-    $this->runCron();
+    $this->cronRun();
     $equal = FALSE;
-    $result = db_query("SELECT id, last, scheduled FROM {job_schedule} WHERE last > %d", $time);
-    while ($row = db_fetch_object($result)) {
+    $rows = db_query("SELECT id, last, scheduled FROM {job_schedule}
+                     WHERE last > %d", $time);
+    while ($row = db_fetch_object($rows)) {
       $equal = $equal && ($row->last == $schedule[$row->id]->last);
       $schedule[$row->id] = $row;
     }
@@ -163,32 +163,55 @@ class FeedsSchedulerTestCase extends Fee
 
     // Hit cron again, 4 times now, every item should change again.
     for ($i = 0; $i < 4; $i++) {
-      $this->runCron();
+      $this->cronRun();
     }
     $equal = FALSE;
-    $result = db_query("SELECT id, last, scheduled FROM {job_schedule} WHERE last > %d", $time);
-    while ($row = db_fetch_object($result)) {
+    $rows = db_query("SELECT id, last, scheduled FROM {job_schedule}
+                     WHERE last > %d", $time);
+    while ($row = db_fetch_object($rows)) {
       $equal = $equal && ($row->last == $schedule[$row->id]->last);
     }
     $this->assertFalse($equal, 'Every feed schedule time changed.');
 
     // There should be 200 story nodes in the database.
-    $count = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story' AND status = 1"));
+    $count = db_result(db_query("SELECT COUNT(*) FROM {node}
+                                WHERE type = 'story' AND status = 1"));
     $this->assertEqual($count, 200, 'The total of 200 story nodes has not changed.');
 
     // Set expire settings, check rescheduling.
-    $max_last = db_result(db_query("SELECT MAX(last) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period = 0"));
-    $min_last = db_result(db_query("SELECT MIN(last) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period = 0"));
-    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_importer_expire' AND last <> 0 AND scheduled = 0")));
-    $this->loginAdmin();
+    $max_last = db_result(db_query("SELECT MAX(last) FROM {job_schedule}
+                                   WHERE type = 'syndication'
+                                   AND callback = 'feeds_source_import'
+                                   AND period = 0"));
+    $min_last = db_result(db_query("SELECT MIN(last) FROM {job_schedule}
+                                   WHERE type = 'syndication'
+                                   AND callback = 'feeds_source_import'
+                                   AND period = 0"));
+    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_importer_expire'
+                                             AND last <> 0 AND scheduled = 0")));
+    $this->drupalLogin($this->admin_user);
     $this->setSettings('syndication', 'FeedsNodeProcessor', array('expire' => 86400));
     $this->drupalLogout();
     sleep(1);
-    $this->runCron();
-    // There should be a feeds_importer_expire callback now, and all last fields should be reset.
-    $this->assertEqual(1, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_importer_expire' AND last <> 0 AND scheduled = 0 AND period = 3600")));
-    $new_max_last = db_result(db_query("SELECT MAX(last) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period = 0"));
-    $new_min_last = db_result(db_query("SELECT MIN(last) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period = 0"));
+    $this->cronRun();
+    // There should be a feeds_importer_expire job now, and all last fields should be reset.
+    $this->assertEqual(1, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_importer_expire'
+                                             AND last <> 0 AND scheduled = 0
+                                             AND period = 3600")));
+    $new_max_last = db_result(db_query("SELECT MAX(last) FROM {job_schedule}
+                                       WHERE type = 'syndication'
+                                       AND callback = 'feeds_source_import'
+                                       AND period = 0"));
+    $new_min_last = db_result(db_query("SELECT MIN(last) FROM {job_schedule}
+                                       WHERE type = 'syndication'
+                                       AND callback = 'feeds_source_import'
+                                       AND period = 0"));
     $this->assertNotEqual($new_max_last, $max_last);
     $this->assertNotEqual($new_min_last, $min_last);
     $this->assertEqual($new_max_last, $new_min_last);
@@ -196,84 +219,162 @@ class FeedsSchedulerTestCase extends Fee
     $min_last = $new_min_last;
 
     // Set import settings, check rescheduling.
-    $this->loginAdmin();
+    $this->drupalLogin($this->admin_user);
     $this->setSettings('syndication', '', array('import_period' => 3600));
     $this->drupalLogout();
     sleep(1);
-    $this->runCron();
-    $new_max_last = db_result(db_query("SELECT MAX(last) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period = 3600"));
-    $new_min_last = db_result(db_query("SELECT MIN(last) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period = 3600"));
+    $this->cronRun();
+    $new_max_last = db_result(db_query("SELECT MAX(last) FROM {job_schedule}
+                                       WHERE type = 'syndication'
+                                       AND callback = 'feeds_source_import'
+                                       AND period = 3600"));
+    $new_min_last = db_result(db_query("SELECT MIN(last) FROM {job_schedule}
+                                       WHERE type = 'syndication'
+                                       AND callback = 'feeds_source_import'
+                                       AND period = 3600"));
     $this->assertNotEqual($new_max_last, $max_last);
     $this->assertNotEqual($new_min_last, $min_last);
     $this->assertEqual($new_max_last, $new_min_last);
-    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND period <> 3600")));
-    $this->assertEqual(1, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_importer_expire' AND period = 3600 AND last = %d", $new_min_last)));
+    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_source_import'
+                                             AND period <> 3600")));
+    $this->assertEqual(1, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_importer_expire'
+                                             AND period = 3600 AND last = %d", $new_min_last)));
 
     // Delete source, delete importer, check schedule.
-    $this->loginAdmin();
+    $this->drupalLogin($this->admin_user);
     $nid = array_shift($nids);
     $this->drupalPost("node/$nid/delete", array(), t('Delete'));
-    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import' AND id = %d", $nid)));
-    $this->assertEqual(count($nids), db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import'")));
-    $this->assertEqual(1, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_importer_expire' AND id =0")));
+    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_source_import'
+                                             AND id = %d", $nid)));
+    $this->assertEqual(count($nids), db_result(db_query("SELECT COUNT(*)
+                                                        FROM {job_schedule}
+                                                        WHERE type = 'syndication'
+                                                        AND callback = 'feeds_source_import'")));
+    $this->assertEqual(1, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_importer_expire'
+                                             AND id = 0")));
 
     $this->drupalPost('admin/build/feeds/delete/syndication', array(), t('Delete'));
-    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_importer_expire' AND id =0")));
-    $this->assertEqual(count($nids), db_result(db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = 'syndication' AND callback = 'feeds_source_import'")));
+    $this->assertEqual(0, db_result(db_query("SELECT COUNT(*)
+                                             FROM {job_schedule}
+                                             WHERE type = 'syndication'
+                                             AND callback = 'feeds_importer_expire'
+                                             AND id = 0")));
+    $this->assertEqual(count($nids), db_result(db_query("SELECT COUNT(*)
+                                                        FROM {job_schedule}
+                                                        WHERE type = 'syndication'
+                                                        AND callback = 'feeds_source_import'")));
   }
 
   /**
    * Test batching on cron.
    */
   function testBatching() {
-    // Verify that there are 150 nodes total.
-    $nid = $this->createFeedNode('syndication', $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/many_items.rss2');
-    $this->assertText('Created 150 Story nodes.');
-    $this->drupalPost('node/'. $nid .'/delete-items', array(), 'Delete');
-    $this->assertText('Deleted 150 nodes.');
+    // Set up an importer.
+    $this->createImporterConfiguration('Node import', 'node');
+    // Set and configure plugins and mappings.
+    $edit = array(
+      'content_type' => '',
+    );
+    $this->drupalPost('admin/build/feeds/edit/node/settings', $edit, 'Save');
+    $this->setPlugin('node', 'FeedsFileFetcher');
+    $this->setPlugin('node', 'FeedsCSVParser');
+    $mappings = array(
+      '0' => array(
+        'source' => 'title',
+        'target' => 'title',
+      ),
+    );
+    $this->addMappings('node', $mappings);
 
-    // Set next time to 0 to simulate updates.
-    db_query("UPDATE {job_schedule} SET next = 0");
-    // Hit cron 3 times, assert correct number of story nodes.
-    for ($i = 0; $i < 3; $i++) {
-      $this->runCron(1);
-      // 50 == FEEDS_NODE_BATCH_SIZE
-      $this->assertEqual(50 * ($i + 1), db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story'")));
-      $this->assertEqual(0, db_result(db_query("SELECT period FROM {job_schedule} WHERE type = 'syndication' AND id = %d", $nid)));
+    // Verify that there are 86 nodes total.
+    $this->importFile('node', $this->absolutePath() .'/tests/feeds/many_nodes.csv');
+    $this->assertText('Created 86 nodes');
+
+    // Run batch twice with two different process limits.
+    // 50 = FEEDS_PROCESS_LIMIT.
+    foreach (array(10, 50) as $limit) {
+      variable_set('feeds_process_limit', $limit);
+
+      db_query("UPDATE {job_schedule} SET next = 0");
+      $this->drupalPost('import/node/delete-items', array(), 'Delete');
+      $this->assertEqual(0, db_result(db_query("SELECT COUNT(*)
+                                               FROM {node} WHERE type = 'story'")));
+
+      // Hit cron (item count / limit) times, assert correct number of stories.
+      for ($i = 0; $i < ceil(86 / $limit); $i++) {
+        $this->cronRun();
+        sleep(1);
+        if ($limit * ($i + 1) < 86) {
+          $count = $limit * ($i + 1);
+          $period = 0; // Import should be rescheduled for ASAP.
+        }
+        else {
+          $count = 86; // We've reached our total of 86.
+          $period = 1800; // Hence we should find the Source's default period.
+        }
+        $this->assertEqual($count, db_result(db_query("SELECT COUNT(*)
+                                                      FROM {node}
+                                                      WHERE type = 'story'")));
+        $this->assertEqual($period, db_result(db_query("SELECT period
+                                                       FROM {job_schedule}
+                                                       WHERE type = 'node'
+                                                       AND id = 0")));
+      }
     }
-    // Run one more time to cause the batch to reset, check period back to 1800.
-    $this->runCron();
-    $this->assertEqual(1800, db_result(db_query("SELECT period FROM {job_schedule} WHERE type = 'syndication' AND id = %d", $nid)));
 
     // Delete a couple of nodes, then hit cron again. They should not be replaced
     // as the minimum update time is 30 minutes.
-    $result = db_query_range("SELECT nid FROM {node} WHERE type = 'story'", 0, 2);
-    while ($node = db_fetch_object($result)) {
+    $nodes = db_query_range("SELECT nid FROM {node} WHERE type = 'story'", 0, 2);
+    while ($node = db_fetch_object($nodes)) {
       $this->drupalPost("node/{$node->nid}/delete", array(), 'Delete');
     }
-    $this->assertEqual(148, db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story'")));
-    $this->runCron();
-    $this->assertEqual(148, db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = 'story'")));
+    $this->assertEqual(84, db_result(db_query("SELECT COUNT(*)
+                                              FROM {node} WHERE type = 'story'")));
+    $this->cronRun();
+    $this->assertEqual(84, db_result(db_query("SELECT COUNT(*)
+                                              FROM {node} WHERE type = 'story'")));
   }
 
   /**
    * Helper, log in as an admin user.
    */
   protected function loginAdmin() {
-    $this->drupalLogin(
-      $this->drupalCreateUser(
-        array(
-          'administer feeds', 'administer nodes',
-        )
+    $this->admin_user = $this->drupalCreateUser(
+      array(
+        'administer feeds', 'administer nodes',
       )
     );
+    $this->drupalLogin($this->admin_user);
+  }
+
+
+  /**
+   * Print schedule as notice.
+   */
+  protected function showSchedule() {
+    $jobs = db_query("SELECT * FROM {job_schedule}");
+    while ($job = db_fetch_object($jobs)) {
+      $this->error('<pre>' . print_r($job, TRUE) . '</pre>');
+    }
   }
 
   /**
-   * Helper, run cron.
+   * Runs cron in the Drupal installed by Simpletest.
    */
-  protected function runCron($sleep = 0) {
-    $this->drupalGet($GLOBALS['base_url'] .'/cron.php');
-    sleep($sleep);
+  protected function cronRun($sleep = 0) {
+    $this->drupalGet($GLOBALS['base_url'] . '/cron.php');
+    sleep(0);
   }
 }
Index: views/feeds.views.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/views/feeds.views.inc,v
retrieving revision 1.3
diff -u -p -r1.3 feeds.views.inc
--- views/feeds.views.inc	11 Jul 2010 16:57:02 -0000	1.3
+++ views/feeds.views.inc	3 Feb 2011 13:04:08 -0000
@@ -65,21 +65,14 @@ function feeds_views_data() {
   );
 
   /**
-   * Expose feeds_node_item table to views.
+   * Expose feeds_item table to views.
    */
-  $data['feeds_node_item']['table'] = array(
-    'group' => 'Feeds Item',
-    'join' => array(
-      'node' => array(
-        'left_field' => 'nid',
-        'field' => 'nid',
-        'type' => 'LEFT',
-      ),
-    ),
+  $data['feeds_item']['table'] = array(
+    'group' => 'Feeds item',
   );
-  $data['feeds_node_item']['feed_nid'] = array(
+  $data['feeds_item']['feed_nid'] = array(
     'title' => t('Owner feed nid'),
-    'help' => t('The node id of the owner feed if available.'),
+    'help' => t('The node id of the owner feed node if available.'),
     'field' => array(
       'handler' => 'views_handler_field_numeric',
       'click sortable' => TRUE,
@@ -100,13 +93,13 @@ function feeds_views_data() {
     ),
     'relationship' => array(
       'title' => t('Owner feed'),
-      'help' => t('Relate a node to its owner feed node if available.'),
+      'help' => t('Relate a feed item to its owner feed node if available.'),
       'label' => t('Owner feed'),
       'base' => 'node',
       'base field' => 'nid',
     ),
   );
-  $data['feeds_node_item']['url'] = array(
+  $data['feeds_item']['url'] = array(
     'title' => t('Item URL'),
     'help' => t('Contains the URL of the feed item.'),
     'field' => array(
@@ -128,7 +121,7 @@ function feeds_views_data() {
       'help' => t('Sort on a Feeds Item\'s URL field.'),
     ),
   );
-  $data['feeds_node_item']['guid'] = array(
+  $data['feeds_item']['guid'] = array(
     'title' => t('Item GUID'),
     'help' => t('Contains the GUID of the feed item.'),
     'field' => array(
@@ -149,7 +142,7 @@ function feeds_views_data() {
       'help' => t('Sort on a Feeds Item\'s GUID field.'),
     ),
   );
-  $data['feeds_node_item']['imported'] = array(
+  $data['feeds_item']['imported'] = array(
     'title' => t('Import date'),
     'help' => t('Contains the import date of the feed item.'),
     'field' => array(
@@ -171,6 +164,176 @@ function feeds_views_data() {
       'help' => t('Argument on a Feeds Item\'s import date field.'),
     ),
   );
+
+  // Add a relationship for each entity type relating the entity's base table
+  // to the feeds_item table whre feeds_item.entity_type = 'entity_type'.
+  $entity_info = array(
+    'node' => array(
+      'base table' => 'node',
+      'id' => 'nid',
+    ),
+    'taxonomy_term' => array(
+      'base table' => 'term_data',
+      'id' => 'tid',
+    ),
+    'user' => array(
+      'base table' => 'user',
+      'id' => 'uid',
+    ),
+  );
+  foreach ($entity_info as $entity_type => $info) {
+    $data['feeds_item']['table']['join'][$info['base table']] = array(
+      'left_field' => $info['id'],
+      'field' => 'entity_id',
+      'type' => 'LEFT',
+      'extra' => array(
+        array(
+          'table' => 'feeds_item',
+          'field' => 'entity_type',
+          'value' => $entity_type,
+          'operator' => '=',
+        ),
+      ),
+    );
+  }
+
+  /**
+   * Expose feeds_log table to views.
+   */
+  $data['feeds_log']['table'] = array(
+    'group' => 'Feeds log',
+    'base' => array(
+      'field' => array('flid'),
+      'title' => 'Feeds log',
+      'help' => 'Logs events during importing, clearing, expiry.',
+    ),
+  );
+  $data['feeds_log']['id'] = array(
+    'title' => 'Importer id',
+    'help' => 'The id of an importer.',
+    'field' => array(
+      'handler' => 'feeds_views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+      'allow empty' => TRUE,
+      'help' => 'Filter on an importer id.',
+    ),
+    'argument' => array(
+      'handler' => 'feeds_views_handler_argument_importer_id',
+      'help' => 'Filter on an importer id.',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+      'help' => 'Sort by importer id.',
+    ),
+    'relationship' => array(
+      'title' => t('Importer'),
+      'help' => t('Relate a log entry to its importer if available.'),
+      'label' => t('Importer'),
+      'base' => 'feeds_importer',
+      'base field' => 'id',
+    ),
+  );
+  $data['feeds_log']['importer_name'] = array(
+    'real field' => 'id',
+    'title' => 'Importer name',
+    'help' => 'The human readable name of an importer.',
+    'field' => array(
+      'handler' => 'feeds_views_handler_field_importer_name',
+    ),
+  );
+  $data['feeds_log']['feed_nid'] = array(
+    'title' => 'Feed node id',
+    'help' => 'Contains the node id of a feed node if the feed\'s configuration is attached to a content type, otherwise contains 0.',
+    'field' => array(
+      'handler' => 'feeds_views_handler_field_numeric',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_numeric',
+      'allow empty' => TRUE,
+      'help' => 'Filter on a Feeds Source\'s feed_nid field.',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_numeric',
+      'numeric' => TRUE,
+      'validate type' => 'nid',
+      'help' => 'Argument on a Feeds Source\'s feed_nid field.',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+      'help' => 'Sort Feeds Source\'s feed_nid field.',
+    ),
+    'relationship' => array(
+      'title' => t('Feed node'),
+      'help' => t('Relate a log entry to its feed node if available.'),
+      'label' => t('Feed node'),
+      'base' => 'node',
+      'base field' => 'nid',
+    ),
+  );
+  $data['feeds_log']['log_time'] = array(
+    'title' => t('Log time'),
+    'help' => t('The time of the event.'),
+    'field' => array(
+      'handler' => 'views_handler_field_date',
+      'click sortable' => TRUE,
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort_date',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_date',
+    ),
+  );
+  $data['feeds_log']['request_time'] = array(
+    'title' => t('Request time'),
+    'help' => t('The time of the page request of an event.'),
+    'field' => array(
+      'handler' => 'views_handler_field_date',
+      'click sortable' => TRUE,
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort_date',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_date',
+    ),
+  );
+  $data['feeds_log']['message'] = array(
+    'title' => 'Log message',
+    'help' => 'The message logged by the event.',
+    'field' => array(
+      'handler' => 'feeds_views_handler_field_log_message',
+      'click sortable' => FALSE,
+      'additional fields' => array(
+        'variables',
+      ),
+    ),
+  );
+  $data['feeds_log']['severity'] = array(
+    'title' => 'Severity',
+    'help' => 'The severity of the event logged.',
+    'field' => array(
+      'handler' => 'feeds_views_handler_field_severity',
+      'click sortable' => FALSE,
+    ),
+    'filter' => array(
+      'handler' => 'feeds_views_handler_filter_severity',
+      'allow empty' => TRUE,
+      'help' => 'Filter on the severity of a log message.',
+    ),
+  );
+  $data['feeds_log']['table']['join'] = array(
+    'node' => array(
+      'left_field' => 'nid',
+      'field' => 'feed_nid',
+      'type' => 'LEFT',
+    ),
+  );
+
   return $data;
 }
 
@@ -184,9 +347,27 @@ function feeds_views_handlers() {
     ),
     'handlers' => array(
       // field handlers
+      'feeds_views_handler_argument_importer_id' => array(
+        'parent' => 'views_handler_argument_string'
+      ),
+      'feeds_views_handler_field_importer_name' => array(
+        'parent' => 'views_handler_field',
+      ),
+      'feeds_views_handler_field_log_message' => array(
+        'parent' => 'views_handler_field',
+      ),
+      'feeds_views_handler_field_severity' => array(
+        'parent' => 'views_handler_field',
+      ),
       'feeds_views_handler_field_source' => array(
         'parent' => 'views_handler_field',
       ),
+      'feeds_views_handler_field_source' => array(
+        'parent' => 'views_handler_field',
+      ),
+      'feeds_views_handler_filter_severity' => array(
+        'parent' => 'views_handler_filter_in_operator',
+      ),
     ),
   );
 }
Index: views/feeds_views_handler_field_source.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/views/feeds_views_handler_field_source.inc,v
retrieving revision 1.2
diff -u -p -r1.2 feeds_views_handler_field_source.inc
--- views/feeds_views_handler_field_source.inc	20 Dec 2009 23:48:38 -0000	1.2
+++ views/feeds_views_handler_field_source.inc	3 Feb 2011 13:04:09 -0000
@@ -29,4 +29,4 @@ class feeds_views_handler_field_source e
   function allow_advanced_render() {
     return FALSE;
   }
-}
\ No newline at end of file
+}
