Newer
Older
Alex Barth
committed
/**
* @file
* Class definition of FeedsNodeProcessor.
*/
// Create or delete FEEDS_NODE_BATCH_SIZE at a time.
define('FEEDS_NODE_BATCH_SIZE', 50);
Alex Barth
committed
/**
* Creates nodes from feed items.
*/
class FeedsNodeProcessor extends FeedsProcessor {
Alex Barth
committed
/**
* Implementation of FeedsProcessor::process().
*/
public function process(FeedsImportBatch $batch, FeedsSource $source) {
Alex Barth
committed
// Keep track of processed items in this pass.
$processed = 0;
Alex Barth
committed
while ($item = $batch->shiftItem()) {
Alex Barth
committed
// Create/update if item does not exist or update existing is enabled.
if (!($nid = $this->existingItemId($item, $source)) || $this->config['update_existing']) {
$node = $this->buildNode($nid, $source->feed_nid);
Alex Barth
committed
// Only proceed if item has actually changed.
$hash = $this->hash($item);
if (!empty($nid) && $hash == $this->getHash($nid)) {
continue;
Alex Barth
committed
}
$node->feeds_node_item->hash = $hash;
// Map and save node. If errors occur don't stop but report them.
try {
$this->map($item, $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)) {
return (1.0 / ($batch->total + 1)) * ($batch->updated + $batch->created); // Add + 1 to make sure that result is not 1.0 = finished.
}
Alex Barth
committed
// Set messages.
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']))));
Alex Barth
committed
}
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']))));
Alex Barth
committed
}
else {
drupal_set_message(t('There is no new content.'));
}
Alex Barth
committed
* Implementation of FeedsProcessor::clear().
*/
public function clear(FeedsBatch $batch, FeedsSource $source) {
if (empty($batch->total)) {
$batch->total = db_result(db_query("SELECT COUNT(nid) FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid));
$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));
Alex Barth
committed
while ($node = db_fetch_object($result)) {
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))) {
return (1.0 / ($batch->total + 1)) * $batch->deleted;
Alex Barth
committed
}
// Set message.
drupal_set_message(format_plural($batch->deleted, 'Deleted @number node.', 'Deleted @number nodes.', array('@number' => $batch->deleted)));
Alex Barth
committed
}
else {
drupal_set_message(t('There is no content to be deleted.'));
}
Alex Barth
committed
}
/**
* Implement expire().
Alex Barth
committed
public function expire($time = NULL) {
if ($time === NULL) {
$time = $this->expiryTime();
}
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));
Alex Barth
committed
while ($node = db_fetch_object($result)) {
Alex Barth
committed
}
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))) {
return FEEDS_BATCH_ACTIVE;
}
return FEEDS_BATCH_COMPLETE;
Alex Barth
committed
}
/**
* Override parent::map() to load all available add-on mappers.
Alex Barth
committed
*/
protected function map($source_item, $target_node) {
self::loadMappers();
Alex Barth
committed
return parent::map($source_item, $target_node);
}
/**
* Return expiry time.
*/
public function expiryTime() {
return $this->config['expire'];
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
$types = node_get_types('names');
$type = isset($types['story']) ? 'story' : key($types);
'content_type' => $type,
Alex Barth
committed
'update_existing' => 0,
'expire' => FEEDS_EXPIRE_NEVER,
Alex Barth
committed
'mappings' => array(),
'author' => 0,
Alex Barth
committed
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$types = node_get_types('names');
$form = array();
$form['content_type'] = array(
'#type' => 'select',
'#title' => t('Content type'),
'#description' => t('Choose node type to create from this feed. <strong>Note:</strong> Users with "import !feed_id feeds" permissions will be able to <strong>import</strong> nodes of the content type selected here regardless of the node level permissions. Further, users with "clear !feed_id permissions" will be able to <strong>delete</strong> imported nodes regardless of their node level permissions.', array('!feed_id' => $this->id)),
Alex Barth
committed
'#options' => $types,
'#default_value' => $this->config['content_type'],
);
$author = user_load(array('uid' => $this->config['author']));
$form['author'] = array(
'#type' => 'textfield',
'#title' => t('Author'),
'#description' => t('Author to be assigned to created nodes - leave empty to assign "anonymous".'),
'#autocomplete_path' => 'user/autocomplete',
'#default_value' => empty($author->name) ? 'anonymous' : check_plain($author->name),
);
Alex Barth
committed
$period = drupal_map_assoc(array(FEEDS_EXPIRE_NEVER, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 604800 * 4, 604800 * 12, 604800 * 24, 31536000), 'feeds_format_expire');
$form['expire'] = array(
'#type' => 'select',
'#title' => t('Expire nodes'),
'#options' => $period,
'#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' => 'checkbox',
'#title' => t('Update existing items'),
'#description' => t('Check if existing items should be updated from the feed.'),
'#default_value' => $this->config['update_existing'],
);
Alex Barth
committed
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
if ($author = user_load(array('name' => $values['author']))) {
$values['author'] = $author->uid;
}
else {
$values['author'] = 0;
}
}
Alex Barth
committed
/**
* 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'))) {
Alex Barth
committed
$target_node->$target_element = $value;
}
}
/**
* Return available mapping targets.
*
* Static cached, may be called multiple times in a page load.
Alex Barth
committed
*/
public function getMappingTargets() {
$targets = array(
'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(
'name' => t('Body'),
'description' => t('The body of the node. The teaser will be the same as the entire body.'),
);
}
$targets += array(
'status' => array(
'name' => t('Published status'),
'description' => t('Whether a node is published or not. 1 stands for published, 0 for not published.'),
),
'created' => array(
'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,
),
);
// Let other modules expose mapping targets.
self::loadMappers();
drupal_alter('feeds_node_processor_targets', $targets, $this->config['content_type']);
Alex Barth
committed
return $targets;
}
/**
* Get nid of an existing feed item node if available.
*/
protected function existingItemId($source_item, FeedsSource $source) {
// Iterate through all unique targets and test whether they do already
// exist in the database.
foreach ($this->uniqueTargets($source_item) as $target => $value) {
switch ($target) {
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));
Alex Barth
committed
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));
Alex Barth
committed
break;
}
if ($nid) {
// Return with the first nid found.
return $nid;
}
}
return 0;
}
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/**
* Creates a new node object in memory and returns it.
*/
protected function buildNode($nid, $feed_nid) {
$node = new stdClass();
if (!empty($nid)) {
$node->nid = $nid;
$node->vid = db_result(db_query("SELECT vid FROM {node} WHERE nid = %d", $nid));
}
else {
$node->created = FEEDS_REQUEST_TIME;
}
$node->type = $this->config['content_type'];
$node->changed = FEEDS_REQUEST_TIME;
$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';
$node->uid = $this->config['author'];
return $node;
}
/**
* Create MD5 hash of $item array.
* @return Always returns a hash, even with empty, NULL, FALSE:
* Empty arrays return 40cd750bba9870f18aada2478b24840a
* Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
*/
protected function hash($item) {
return hash('md5', serialize($item));
}
/**
* 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)));