diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc index 0644feb..6a1111e 100644 --- a/plugins/FeedsProcessor.inc +++ b/plugins/FeedsProcessor.inc @@ -32,6 +32,11 @@ class FeedsValidationException extends Exception {} class FeedsAccessException extends Exception {} /** + * Thrown if an entity could not be loaded. + */ +class FeedsEntityNotFoundException extends Exception {} + +/** * Abstract class, defines interface for processors. */ abstract class FeedsProcessor extends FeedsPlugin { @@ -116,6 +121,9 @@ abstract class FeedsProcessor extends FeedsPlugin { * * @todo We should be able to batch load these, if we found all of the * existing ids first. + * + * @throws FeedsEntityNotFoundException + * When an entity could not be loaded. */ protected function entityLoad(FeedsSource $source, $entity_id) { $info = $this->entityInfo(); @@ -131,7 +139,13 @@ abstract class FeedsProcessor extends FeedsPlugin { $entity = db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject(); } - if ($entity && !empty($info['entity keys']['language'])) { + if (empty($entity)) { + throw new FeedsEntityNotFoundException(t('Entity with id @entity_id could not be loaded.', array( + '@entity_id' => $entity_id, + ))); + } + + if (!empty($info['entity keys']['language'])) { $entity->{$info['entity keys']['language']} = $this->entityLanguage(); } @@ -236,8 +250,8 @@ abstract class FeedsProcessor extends FeedsPlugin { $this->initEntitiesToBeRemoved($source, $state); } - $skip_new = $this->config['insert_new'] == FEEDS_SKIP_NEW; - $skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING; + $insert_new = $this->config['insert_new'] != FEEDS_SKIP_NEW; + $update_existing = $this->config['update_existing'] != FEEDS_SKIP_EXISTING; while ($item = $parser_result->shiftItem()) { @@ -250,39 +264,34 @@ abstract class FeedsProcessor extends FeedsPlugin { module_invoke_all('feeds_before_update', $source, $item, $entity_id); - // If it exists, and we are not updating, or if it does not exist, and we - // are not inserting, pass onto the next item. - if (($entity_id && $skip_existing) || (!$entity_id && $skip_new)) { - continue; - } - try { $hash = $this->hash($item); - $changed = $hash !== $this->getHash($entity_id); // Do not proceed if the item exists, has not changed, and we're not // forcing the update. - if ($entity_id && !$changed && !$this->config['skip_hash_check']) { + if ($entity_id && !$this->config['skip_hash_check'] && $update_existing) { + if ($hash === $this->getHash($entity_id)) { + // The item has not changed. + continue; + } + } + + // Load or create the entity. + $entity = $this->provideEntity($source, $entity_id, $update_existing, $insert_new); + if (empty($entity)) { + // No entity was found nor created. Go to the next item. continue; } - // Load an existing entity. + // Add item info. + $this->newItemInfo($entity, $source->feed_nid, $hash); if ($entity_id) { - $entity = $this->entityLoad($source, $entity_id); - // The feeds_item table is always updated with the info for the most // recently processed entity. The only carryover is the entity_id. - $this->newItemInfo($entity, $source->feed_nid, $hash); $entity->feeds_item->entity_id = $entity_id; $entity->feeds_item->is_new = FALSE; } - // Build a new entity. - else { - $entity = $this->newEntity($source); - $this->newItemInfo($entity, $source->feed_nid, $hash); - } - // Set property and field values. $this->map($source, $parser_result, $entity); $this->entityValidate($entity); @@ -411,6 +420,56 @@ abstract class FeedsProcessor extends FeedsPlugin { } /** + * Loads an existing entity or creates new one. + * + * The entity_id parameter is emptied if an existing entity could not + * be found or if creating a new entity. + * + * Called by the process() method. + * + * @param FeedsSource $source + * The feeds source that spawns this entity. + * @param int|string $entity_id + * (optional) The id of the entity to load. + * @param bool $load_existing + * (optional) If the entity may be loaded at all. + * Defaults to true. + * @param bool $create_new + * (optional) If a new entity may be created. + * Defaults to true. + * + * @return object|null + * An entity if loading or creation was succesful. + * NULL otherwise. + * + * @see ::process() + */ + protected function provideEntity(FeedsSource $source, &$entity_id = NULL, $load_existing = TRUE, $create_new = TRUE) { + $entity = NULL; + + if ($entity_id && $load_existing) { + // Try to load an existing entity. + try { + $entity = $this->entityLoad($source, $entity_id); + } + catch (FeedsEntityNotFoundException $e) { + // In case the entity could not be found, empty the entity_id and act as if + // the item to import is new. + $entity_id = NULL; + // Log the exception. + $source->log('import', '@exception', array('@exception' => $e->getMessage()), WATCHDOG_WARNING); + } + } + + if (!$entity_id && $create_new) { + // Build a new entity. + $entity = $this->newEntity($source); + } + + return $entity; + } + + /** * Initializes the list of entities to remove. * * This populates $state->removeList with all existing entities previously diff --git a/tests/feeds_processor_node.test b/tests/feeds_processor_node.test index 8c1893a..6b1da8e 100644 --- a/tests/feeds_processor_node.test +++ b/tests/feeds_processor_node.test @@ -721,4 +721,56 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase { $this->assertText('There are no new nodes.'); } + /** + * Tests if importing items does not result into errors if the feeds_item contains references + * to entities that no longer exist. + * + * @todo Add a test for an item that points to a non-existing feed node as well. + */ + public function testOrphanedFeedsItem() { + // Include FeedsProcessor.inc so processor related constants are available. + module_load_include('inc', 'feeds', 'plugins/FeedsProcessor'); + + // Create a feeds item with a reference to a non-existing entity. + // Note that the GUID value is used in the file that gets imported later. + $item = array( + 'entity_type' => 'node', + 'entity_id' => 120, + 'id' => 'syndication', + 'feed_nid' => 0, + 'imported' => REQUEST_TIME -1, + 'url' => '', + 'guid' => 1, + 'hash' => '', + ); + drupal_write_record('feeds_item', $item); + + // Attach to standalone importer and configure. + $this->setSettings('syndication', NULL, array('content_type' => '')); + $this->setPlugin('syndication', 'FeedsFileFetcher'); + $this->setPlugin('syndication', 'FeedsCSVParser'); + $this->setSettings('syndication', 'FeedsNodeProcessor', array( + 'update_existing' => FEEDS_UPDATE_EXISTING, + )); + $this->removeMappings('syndication', $this->getCurrentMappings('syndication')); + $this->addMappings('syndication', array( + 0 => array( + 'source' => 'guid', + 'target' => 'guid', + 'unique' => TRUE, + ), + 1 => array( + 'source' => 'title', + 'target' => 'title', + ), + )); + + // Import file. + $this->importFile('syndication', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Although for one item in the file a reference existed in the feeds_item table, both + // items did not exist on the website, so we expect that for both items a node is created. + $this->assertText('Created 2 nodes'); + } + }