'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': $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 db_query("UPDATE {feedapi} SET url = '%s', feed_type = '%s', processors = '%s', parsers = '%s', refresh = %d, items_delete = %d WHERE nid = %d", $node->feed->url, $node->feed->feed_type, serialize($node->feed->processors), serialize($node->feed->parsers), $node->feedapi['feedapi_refresh'], $node->feedapi['feedapi_items_delete'], $node->feed->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 'view': if (!$teaser) { if (is_array($node->feed->processors)) { if (in_array('feedapi_item', $node->feed->processors)) { $items = module_invoke('feedapi_item', 'feedapi_item_fetch_items', $node->feed); for ($i = count($items) - 1; $i != count($items) - 4 && $i != 0; $i--) { $list[] = l($items[$i]->title, 'node/'. $items[$i]->nid); } $node->content['body']['#value'] .= theme_item_list($list); } } } break; } } } /** * Implementation of hook_perm(). */ function feedapi_perm() { return array('administer feedapi'); } /** * 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) { $help = $parser .'_help'; $name = function_exists($help) ? $help('feedapi/full_name') : $parser; $form['feedapi']['parsers'][$parser] = array( '#type' => 'fieldset', '#title' => strlen($name) > 0 ? $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) { $help = $processor .'_help'; $name = function_exists($help) ? $help('feedapi/full_name') : $parser; $form['feedapi']['processors'][$processor] = array( '#type' => 'fieldset', '#title' => strlen($name) > 0 ? $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'])) { // 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'] = t($k); // Todo: human readable name, retrievable by module name. $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'] = t($m); // Todo: human readable name, retrievable by module name. $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 { foreach (array_keys($form) as $k) { if (strpos($k, '#') !== 0) { unset($form[$k]); } } $form['feedapi']['feedapi_url'] = array( '#type' => 'textfield', '#title' => t('Feed URL'), '#description' => t('Enter feed URL.'), ); $form['feedapi']['next'] = array( '#type' => 'button', '#value' => t('Submit and edit'), ); $form['feedapi']['submit'] = array( '#type' => 'button', '#value' => t('Submit'), ); } // Process posted URL from short form. // Todo: see, how we can move this into hook_nodeapi('submit', ...). 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'))) ) { // 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('URL'), 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']); } } $url_text = strlen($node->feed->url) < 50 ? $node->feed->url : substr($node->feed->url, 0, 50). "..."; $rows[] = array(l($url_text, $node->feed->url), format_interval($node->feed->refresh), $node->feed->checked == 0 ? t('Never') : format_interval(time() - $node->feed->checked) ." ". t("ago"), $node->title, theme_item_list($commands) ); } return theme_table($header, $rows); } /** * Settings: * Allowed HTML tags, number of feeds refreshed in one round */ function feedapi_admin_settings() { $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', '