Feeds are based on content-types. Before you can create a feed, you have to go to the admin/content/types/add page. There choose a name for the content-type, then enable the "Is a feed content type" checkbox under the Feed API group. Next choose the processors and parsers that you want to use. At least one parser and one processor must be enabled. Then create the content-type. Make sure that permissions are given to the users who will handle the feeds. '); } } /** * Implementation of hook_menu(). */ function feedapi_menu($may_cache) { if ($may_cache) { $items[] = array( 'path' => 'admin/content/feed', 'title' => t('Feed'), 'callback' => 'feedapi_management_page', 'access' => user_access('administer feedapi'), ); $items[] = array( 'path' => 'admin/content/feed/list', 'title' => t('List'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -15, ); $items[] = array('path' => 'admin/settings/feedapi', 'title' => t('FeedAPI settings'), 'callback' => 'drupal_get_form', 'callback arguments' => array('feedapi_admin_settings'), 'type' => MENU_NORMAL_ITEM, 'access' => user_access('administer feedapi'), ); } else if (arg(0) == 'node' && is_numeric(arg(1))) { $node = node_load(arg(1)); if (isset($node->feed)) { global $user; $own_feed = $node->uid == $user->uid && user_access('edit own '. $node->type . ' content') ? TRUE : FALSE; $items[] = array('path' => 'node/'. $node->nid. '/refresh', 'title' => t('Refresh'), 'callback' => 'feedapi_invoke_feedapi', 'callback arguments' => array("refresh", $node->feed), 'type' => MENU_LOCAL_TASK, 'access' => variable_get('feedapi_feed_show_commands_link', TRUE) && (user_access('administer feedapi') || $own_feed), ); } } return $items; } /** * Implementation of hook_nodeapi(). */ function feedapi_nodeapi(&$node, $op, $teaser, $page) { if (isset($node->feed) || _feedapi_is_enabled($node->type)) { switch ($op) { case 'insert': $node->feed->nid = $node->nid; if (isset($node->feed->url) && isset($node->feed->feed_type)) { db_query("INSERT INTO {feedapi} ( nid, url, link, feed_type, processors, parsers, refresh, checked) VALUES (%d, '%s', '%s', '%s', '%s', '%s', %d, %d)", $node->feed->nid, $node->feed->url, $node->feed->options->link, $node->feed->feed_type, serialize($node->feed->processors), serialize($node->feed->parsers), 900, 0 ); _feedapi_store_settings(array('nid' => $node->nid), $node->feed->settings); } break; case 'update': if (isset($node->feed)) { $old_config = node_load($node->nid); foreach ($old_config->feed->processors as $processor) { if (!in_array($processor, $node->feed->processors)) { $items = module_invoke($processor, 'feedapi_item_fetch_items', $node->feed); foreach ($items as $item) { module_invoke($processor, 'feedapi_item_delete', $item); } } } // Store the common things if ($node->url_updated == TRUE) { db_query("UPDATE {feedapi} SET url = '%s', link = '%s', refresh = %d, items_delete = %d WHERE nid = %d", $node->feed->url, $node->feed->options->link, $node->feedapi['feedapi_refresh'], $node->feedapi['feedapi_items_delete'], $node->nid ); } else { db_query("UPDATE {feedapi} SET refresh = %d, items_delete = %d WHERE nid = %d", $node->feedapi['feedapi_refresh'], $node->feedapi['feedapi_items_delete'], $node->nid ); } _feedapi_store_settings(array('nid' => $node->nid), $node->feedapi); } break; case 'load': $node->feed = db_fetch_object(db_query('SELECT * FROM {feedapi} WHERE nid = %d', $node->nid)); $node->feed->nid = $node->nid; $node->feed->settings = _feedapi_get_settings(array('nid' => $node->nid)); $node->feed->processors = unserialize($node->feed->processors); $node->feed->parsers = unserialize($node->feed->parsers); break; case 'delete': // Could be a performance problem - think of thousands of node feed items. feedapi_invoke_feedapi('purge', $node->feed); db_query("DELETE FROM {feedapi} WHERE nid = %d", $node->nid); break; case 'submit': if (!isset($node->nid)) { // Here the full node form will be processed $feed->url = $node->feedapi_url; $settings = _feedapi_get_settings(array('node_type' => $node->type)); $assigned = _feedapi_assign_parser_processor($feed->url, $settings); $feed->processors = $assigned['processors']; $feed->parsers = $assigned['parsers']; $feed->feed_type = $assigned['type']; $feed = _feedapi_call_parsers($feed, $feed->parsers); $feed->settings = $settings; $node->feed = $feed; if (empty($node->title)) { $node->title = $feed->title; } if (empty($node->body)) { $node->body = $feed->description; } } else { // @todo: maybe alter the node-editing form to empty title and body field if the url is altered $old_config = node_load($node->nid); if ($node->feedapi['feedapi_url'] != $old_config->feed->url) { $feed->url = $node->feedapi['feedapi_url']; $feed = _feedapi_call_parsers($feed, $old_config->feed->parsers); $node->title = empty($node->title) ? $feed->title : $node->title; $node->body = empty($node->body) ? $feed->description : $node->body; $node->feed->options->link = $feed->options->link; $node->feed->url = $feed->url; $node->url_updated = TRUE; } } break; } } } /** * Implementation of hook_perm(). */ function feedapi_perm() { return array('administer feedapi', 'advanced feedapi options'); } /** * Implementation of hook_link(). */ function feedapi_link($type, $node = NULL, $teaser = FALSE) { if ($type == 'node' && isset($node->feed)) { if (variable_get('feedapi_feed_show_feed_origin_link', TRUE) && strlen($node->feed->link) > 0) { $links['feedapi_original'] = array( 'title' => t('Link to the site'), 'href' => $node->feed->link, ); return $links; } } } /** * Do various things with feed. Handle the core data and call the * underyling modules (parsers/processors) too * * @param $op * "load" Load the feed items basic data into the $feed->items[] * "refresh" Re-download the feed and process newly arrived item * * @param $feed * A feed object. If only the ID is known, you should pass something like this: $feed->nid = X * @param $param * Depends on the $op value. */ function feedapi_invoke_feedapi($op, &$feed, $param = NULL) { if (!is_object($feed)) { return FALSE; } if (!isset($feed->processors)) { $node = node_load($feed->nid); if (!isset($node->feed)) { return FALSE; } $feed = $node->feed; } _feedapi_sanitize_processors($feed); switch ($op) { case 'load': $feed->items = array(); foreach ($feed->processors as $weight) { foreach ($weight as $processor) { $items = module_invoke($processor, "feedapi_item_fetch_items", $feed); if (is_array($items)) { foreach ($items as $item) { $feed->items[] = $item; } } } } break; case 'refresh': $cron = $param; feedapi_invoke_feedapi("load", $feed); if (!is_array($feed->processors) || count($feed->processors) == 0) { if (!$cron) { drupal_set_message(t("This feed (%url) doesn't have any processor. It's not possible to refresh the feed.", array('%url' => $feed->url)), "error"); drupal_goto('node/'. $feed->nid); } return; } $processors = $feed->processors; // Force the processors to delete old items and determine the max. create elements $max_new_items = variable_get('feedapi_refresh_once', 10); $refresh = feedapi_expire($feed, $cron === TRUE ? FALSE : TRUE); if ($refresh == FALSE) { return; } $feed = _feedapi_call_parsers($feed, $feed->parsers); $feed->processors = $processors; $items = array_reverse($feed->items); $updated = 0; $new = 0; // Walk through the items foreach ($items as $item) { // Call each item parser $is_updated = FALSE; $is_new = FALSE; foreach ($feed->processors as $weight) { foreach ($weight as $processor) { if (!module_invoke($processor, 'feedapi_item_unique', $item, $feed->nid, $feed->settings['processors'][$processor])) { module_invoke($processor, 'feedapi_item_update', $item, $feed->nid, $feed->settings['processors'][$processor]); $is_updated = TRUE; } else { module_invoke($processor, 'feedapi_item_save', $item, $feed->nid, $feed->settings['processors'][$processor]); $is_new = TRUE; } } } $new = $is_new ? $new + 1 : $new; $updated = ($is_updated && !$is_new) ? $updated + 1 : $updated; // If we consumed the max new item limit -> stop if ($max_new_items == $new) { break; } } db_query("UPDATE {feedapi} SET checked = %d WHERE nid = %d", time(), $feed->nid); foreach (module_implements('feedapi_after_refresh') as $module) { $func = $module. '_feedapi_after_refresh'; $func($feed); } if (!$cron) { drupal_set_message(t("%new new item(s) were saved. %updated existing item(s) were updated.", array("%new" => $new, "%updated" => $updated))); drupal_goto('node/'. $feed->nid); } break; case 'purge': feedapi_invoke_feedapi("load", $feed); // Delete items from the processors foreach ($feed->items as $item) { foreach($feed->processors as $weight) { foreach ($weight as $processor) { // FIXME: it's possible now to delete an item from another processor as an incident module_invoke($processor, 'feedapi_item_delete', $item, $feed->settings['processors'][$processor]); } } } break; } } /** * Delete expired items and return informations about the feed refreshing * * @param $feed * The feed object * @param $force * If TRUE, do not examine that the feed needs refresh * @return * FALSE if the feed don't have to be refreshed. (forbidden if the $force is TRUE) */ function feedapi_expire($feed, $force) { $needs_refresh = time() - $feed->checked > $feed->refresh; if (($needs_refresh || $force) && $feed->items_delete != FEEDAPI_NEVER_DELETE_OLD) { foreach ($feed->items as $item) { if (isset($item->arrived) || isset($item->timestamp)) { $diff = abs(time() - (isset($item->arrived) ? $item->arrived : $item->timestamp)); if ($diff > $feed->items_delete) { foreach($feed->processors as $weight) { foreach ($weight as $processor) { module_invoke($processor, 'feedapi_item_delete', $item, $feed->nid); } } } } } } if ($needs_refresh == FALSE && $force == FALSE) { return FALSE; } return TRUE; } /** * Implementation of hook_form_alter(). */ function feedapi_form_alter($form_id, &$form) { // Content type form. if ($form_id == 'node_type_form' && isset($form['identity']['type'])) { if (isset($form['#post']['feedapi'])) { // TODO: Drupal automatically stores mutilated 'feedapi_'. $form['#node_type']->type - remove. // TODO: Do better validation $type = !empty($form['#node_type']->type) ? $form['#node_type']->type : $form['#post']['type']; _feedapi_store_settings(array('node_type' => $type), $form['#post']['feedapi']); } if (!$settings = _feedapi_get_settings(array('node_type' => $form['#node_type']->type))) { $settings = array(); } $form['feedapi'] = array( '#type' => 'fieldset', '#title' => t('Feed API'), '#collapsible' => TRUE, '#collapsed' => !$settings['enabled'], '#tree' => TRUE, ); $form['feedapi']['enabled'] = array( '#type' => 'checkbox', '#title' => t('Is a feed content type'), '#description' => t('Check this box if you want to use this content type for downloading feeds to your site.'), '#default_value' => $settings['enabled'], '#weight' => -15, ); $form['feedapi']['parsers'] = array( '#type' => 'fieldset', '#title' => t('Parser settings'), '#description' => t('Parsers create an object ready for processing from a feed, choose at least one.'), '#collapsible' => FALSE, '#tree' => TRUE, ); $parsers = _feedapi_suitable_parsers('', TRUE); rsort($parsers); foreach ($parsers as $parser) { $form['feedapi']['parsers'][$parser] = array( '#type' => 'fieldset', '#title' => feedapi_get_natural_name($parser), '#collapsible' => TRUE, '#collapsed' => !($settings['parsers'][$parser]['enabled'] || (count($parsers) == 1)), '#tree' => TRUE, '#weight' => $settings['parsers'][$parser]['weight'], ); $form['feedapi']['parsers'][$parser]['enabled'] = array( '#type' => 'checkbox', '#title' => t('Enable'), '#description' => t('Check this box if you want to enable the @name parser on this feed.', array('@name' => t($parser))), '#default_value' => $settings['parsers'][$parser]['enabled'] || (count($parsers) == 1), '#weight' => -15, ); $form['feedapi']['parsers'][$parser]['weight'] = array( '#type' => 'weight', '#delta' => 15, '#title' => t('Weight'), '#description' => t('Control the execution order. Parsers with lower weights come before parsers with higher weights.'), '#options' => $options, '#default_value' => $settings['parsers'][$parser]['weight'], '#weight' => -14, ); } $form['feedapi']['processors'] = array( '#type' => 'fieldset', '#title' => t('Processor settings'), '#description' => t('Processors are any kind of add on modules that hook into the feed handling process on download time - you can decide here what should happen to feed items once they are downloaded and parsed.'), '#collapsible' => FALSE, '#tree' => TRUE, ); $processors = _feedapi_suitable_processors('', TRUE); rsort($processors); foreach ($processors as $processor) { $form['feedapi']['processors'][$processor] = array( '#type' => 'fieldset', '#title' => feedapi_get_natural_name($processor), '#collapsible' => TRUE, '#collapsed' => !($settings['processors'][$processor]['enabled'] || (count($processors) == 1)), '#tree' => TRUE, '#weight' => $settings['processors'][$processor]['weight'], ); $form['feedapi']['processors'][$processor]['enabled'] = array( '#type' => 'checkbox', '#title' => t('Enable'), '#description' => t('Check this box if you want to enable the @name processor on this feed.', array('@name' => t($processor))), '#default_value' => $settings['processors'][$processor]['enabled'], '#weight' => -15, ); $form['feedapi']['processors'][$processor]['weight'] = array( '#type' => 'weight', '#delta' => 15, '#title' => t('Weight'), '#description' => t('Control the execution order. Processors with lower weights come before processors with higher weights.'), '#default_value' => $settings['processors'][$processor]['weight'], '#weight' => -14, ); } $form['feedapi'] = array_merge_recursive($form['feedapi'], _feedapi_invoke_settings_form(array('node_type' => $form['#node_type']->type))); $submit = $form['submit']; $delete = $form['delete']; unset($form['submit']); unset($form['delete']); $form['submit'] = $submit; $form['delete'] = $delete; } // FeedAPI-enabled node form. if ($form['type']['#value'] .'_node_form' == $form_id && _feedapi_is_enabled($form['type']['#value'])) { $form['title']['#required'] = FALSE; // Modify form - add feedapi form snippet in submit-and-edit and edit mode... if (isset($form['#node']->nid) || (isset($form['#post']['op']) && $form['#post']['op'] != 'Submit')) { $form['feedapi'] = array( '#type' => 'fieldset', '#title' => t('Feed API'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, ); $form['feedapi']['feedapi_url'] = array( '#type' => 'textfield', '#title' => t('Feed URL'), '#description' => t('Enter feed URL.'), '#default_value' => $form['#node']->feed->url ? $form['#node']->feed->url : $form['#post']['feedapi_url'], ); $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval'); $form['feedapi']['feedapi_refresh'] = array( '#type' => 'select', '#options' => $period, '#title' => t('Refresh interval'), '#default_value' => $form['#node']->feed->refresh ? $form['#node']->feed->refresh : $form['#post']['feedapi_refresh'], ); $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 3628800, 4838400, 7257600, 15724800, 31536000), 'format_interval'); $period[FEEDAPI_NEVER_DELETE_OLD] = t('Never'); $form['feedapi']['feedapi_items_delete'] = array( '#type' => 'select', '#title' => t('Delete news items older than'), '#options' => $period, '#default_value' => $form['#node']->feed->items_delete ? $form['#node']->feed->items_delete : $form['#post']['feedapi_items_delete'], ); // Retrieve form snippets from add on modules. // Todo: only let users with "edit own feed settings" permission or "administer feeds" permission meddle with these per-node settings. $form_snippets = _feedapi_invoke_settings_form(array('node_type' => $form['type']['#value'], 'nid' => $form['#node']->nid)); // Format snippets - make fieldsets out of them. foreach ($form_snippets as $k => $stage) { $form_snippets[$k]['#type'] = 'fieldset'; $form_snippets[$k]['#title'] = feedapi_get_natural_name($k); $form_snippets[$k]['#collapsible'] = TRUE; $form_snippets[$k]['#collapsed'] = TRUE; $form_snippets[$k]['#tree'] = TRUE; foreach (array_keys($stage) as $m) { $form_snippets[$k][$m]['#type'] = 'fieldset'; $form_snippets[$k][$m]['#title'] = feedapi_get_natural_name($m); $form_snippets[$k][$m]['#collapsible'] = TRUE; $form_snippets[$k][$m]['#collapsed'] = FALSE; $form_snippets[$k][$m]['#tree'] = TRUE; } } $form_snippets['#tree'] = TRUE; $form['feedapi'] = array_merge_recursive($form['feedapi'], $form_snippets); } // ... or wipe form and replace it by a short form in add mode. else { if (variable_get('feedapi_form_create', 'full') != 'full') { foreach (array_keys($form) as $k) { if (strpos($k, '#') !== 0) { unset($form[$k]); } } } if (variable_get('feedapi_form_create', 'full') == 'full') { $form['feedapi'] = array( '#type' => 'fieldset', '#title' => t('FeedAPI'), '#collapsible' => TRUE, '#collapsed' => FALSE, ); } $form['feedapi']['feedapi_url'] = array( '#type' => 'textfield', '#title' => t('Feed URL'), '#description' => t('Enter feed URL.'), ); if (variable_get('feedapi_form_create', 'full') != 'full') { $form['feedapi']['next'] = array( '#type' => 'button', '#value' => t('Submit and edit'), ); $form['feedapi']['submit'] = array( '#type' => 'button', '#value' => t('Submit'), ); } } // Process posted URL from only short form. // See hook_nodeapi('submit') for the long form process if (count($form['#post']) && !isset($form['#node']->nid)) { if (!$form['#post']['feedapi_url']) { form_set_error('feedapi_url', t('Enter a URL')); } else if (db_result(db_query("SELECT COUNT(*) FROM {feedapi} WHERE url = '%s'", $form['#post']['feedapi_url'])) > 0) { form_set_error('feedapi_url', t('This URL (%url) is already created.', array('%url' => $form['#post']['feedapi_url']))); } else if (in_array($form['#post']['op'], array(t('Submit'), t('Submit and edit'))) && variable_get('feedapi_form_create', 'full') != 'full') { // Try to pull title and description directly from feed. // Todo: pull icons and logos in add on module. $feed = new stdClass(); $feed->url = $form['#post']['feedapi_url']; $settings = _feedapi_get_settings(array('node_type' => $form['#node']->type)); $assigned = _feedapi_assign_parser_processor($feed->url, $settings); $feed->link = $feed->options->link; $feed->processors = $assigned['processors']; $feed->parsers = $assigned['parsers']; $feed->feed_type = $assigned['type']; if (empty($feed->feed_type)) { form_set_error('feedapi', t('Error parsing feed.')); } else { $feed->type = $assigned['type']; $feed = _feedapi_call_parsers($feed, $feed->parsers); } if (!$feed->title) { form_set_error('feedapi_url', t('Feed information could not be retrieved.')); } else { $feed->settings = $settings; $node = _feedapi_create_node($feed, $form['#node']->type); if ($form['#post']['op'] == t('Submit')) { drupal_goto('node/'. $node->nid); } else if ($form['#post']['op'] == t('Submit and edit')) { drupal_goto('node/'. $node->nid .'/edit'); } } } } } } /** * Implementation of hook_cron(). */ function feedapi_cron() { $result = db_query_range("SELECT nid FROM {feedapi} ORDER BY checked ASC", 0, variable_get('feedapi_cron_max', 5)); while ($feed = db_fetch_array($result, $row++)) { $node = node_load($feed['nid']); feedapi_invoke_feedapi('refresh', $node->feed, TRUE); } } /** * Provide a UI for overviewing the existing feeds */ function feedapi_management_page() { $header = array(t('Preset'), t('Refresh interval'), t('Last refresh'), t('Title'), t('Commands') ); $rows = array(); $result = db_query("SELECT nid from {feedapi} ORDER BY checked DESC"); while ($nid = db_result($result, $row++)) { $node = node_load($nid); $commands = array(l(t('Delete'), 'node/'. $node->nid . '/delete'), l(t('Refresh'), 'node/'. $node->nid . '/refresh'), l(t('Edit'), 'node/'. $node->nid . '/edit'), ); $ext_commands = module_invoke_all('feedapi_edit_option', $node->feed); if (count($ext_commands) > 0) { foreach ($ext_commands as $command) { $commands[] = l($command['name'], $command['link']); } } $preset = node_get_types('type', $node); $rows[] = array( l($preset->name, 'admin/content/types/'. $preset->type), format_interval($node->feed->refresh), $node->feed->checked == 0 ? t('Never') : format_interval(time() - $node->feed->checked) ." ". t("ago"), l($node->title, $node->feed->link), theme_item_list($commands) ); } return theme_table($header, $rows); } /** * Get the module-defined natural name of FeedAPI parser or processor * If your module want to define such a name: * * function hook_help($section) { * switch ($section) { * case 'feedapi/full_name': * return t('Natural name'); * break; * } * } */ function feedapi_get_natural_name($module) { $help = $module .'_help'; $module_natural = function_exists($help) ? $help('feedapi/full_name') : t($module); return empty($module_natural) ? t($module) : $module_natural; } /** * Settings: * Allowed HTML tags, number of feeds refreshed in one round */ function feedapi_admin_settings() { $form['feedapi_form_create'] = array( '#type' => 'radios', '#title' => t('Feed create form style'), '#default_value' => variable_get('feedapi_form_create', 'full'), '#options' => array('full' => t('Full'), 'simple' => 'Simplified'), '#description' => t('If it is set full, the feed creation form will be a full node creating form. Otherwise a simplified form will be shown.') ); $form['feedapi_allowed_html_tags'] = array( '#type' => 'textfield', '#title' => t('Allowed HTML tags'), '#size' => 80, '#maxlength' => 255, '#default_value' => variable_get('feedapi_allowed_html_tags', '