'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' => 'feed/add', 'title' => t('Create feed'), 'callback' => 'drupal_get_form', 'callback arguments' => array('feedapi_add_page'), 'access' => user_access('handle own feeds') || user_access('administer feedapi'), ); $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) == 'feed' && is_numeric(arg(1))) { global $user; $feed = new stdClass(); $feed->fid = arg(1); feedapi_invoke_feedapi("load", $feed); $own_feed = $feed->uid == $user->uid && user_access('handle own feeds') ? TRUE : FALSE; $items[] = array('path' => 'feed/'. arg(1) .'/delete', 'title' => t('Delete'), 'callback' => 'drupal_get_form', 'callback arguments' => array('feedapi_delete_confirm', $feed), 'access' => user_access('administer feedapi') || $own_feed, 'type' => MENU_CALLBACK ); $items[] = array('path' => 'feed/'. arg(1) .'/refresh', 'title' => t('Refresh'), 'callback' => 'feedapi_invoke_feedapi', 'callback arguments' => array("refresh", $feed), 'access' => user_access('administer feedapi') || $own_feed, 'type' => MENU_CALLBACK ); $items[] = array('path' => 'feed/'. arg(1) .'/edit', 'title' => t('Edit'), 'callback' => 'drupal_get_form', 'callback arguments' => array('feedapi_edit_page', $feed), 'access' => user_access('administer feedapi') || $own_feed, 'type' => MENU_CALLBACK ); } return $items; } /** * Implementation of hook_perm(). */ function feedapi_perm() { return array('administer feedapi', 'handle own feeds'); } /** * Do various things with feed. Handle the core data and call the * underyling modules (parsers/processors) too * * @param $op * "load" Load the feed. $param can be: * "no" - the feed object won't have items member * "info" - only the nid, fid, fiid data members will put ito each item * "full" - the whole stored structure will be fetched into items * "refresh" Re-download the feed and process newly arrived item * "purge" Delete the feed and all connected things (eg. items) * "save" Save the feed * "update" Update the feed * * FIXME: maybe split into private functions * * @param $feed * A feed object. If only the ID is known, you should pass something like this: $feed->fid = X * @param $param * Depends on the $op value. */ function feedapi_invoke_feedapi($op, &$feed, $param = NULL) { if (!is_object($feed)) { return FALSE; } $user = isset($feed->user) || isset($feed->uid) ? user_load(array('name' => $feed->user, 'uid' => $feed->uid)) : NULL; if (!isset($user->uid) || $user->uid == 0) { global $user; } _feedapi_sanitize_processors($feed); switch ($op) { case 'load': $load_items = $param; $feed = db_fetch_object(db_query("SELECT * FROM {feedapi} WHERE fid = %d", $feed->fid)); $feed->parsers = unserialize($feed->parsers); $feed->processors_item = unserialize($feed->processors_item); $feed->processors_feed = unserialize($feed->processors_feed); _feedapi_sanitize_processors($feed); $feed->options = new stdClass(); // Load additional elements provided by the processors foreach ($feed->processors_feed as $processor) { $feed = module_invoke($processor, "feedapi_load", $feed); } if ($load_items == "info" || $load_items == "full") { $feed->items = array(); foreach ($feed->processors_item as $processor) { $items = module_invoke($processor, "feedapi_item_fetch_items", $feed); $feed->items += is_array($items) ? $items : array(); } } if ($load_items == "full") { foreach ($feed->items as $key => $item) { foreach ($feed->processors_item as $processor) { $item = module_invoke($processor, "feedapi_item_load", $item); $feed->items[$key] = $item; } } } module_invoke_all('feedapi_after_load', $feed); break; case 'refresh': $cron = $param; $goto = user_access('administer feedapi') ? 'admin/content/feed' : drupal_init_path(); feedapi_invoke_feedapi("load", $feed, "info"); if (!is_array($feed->processors_item) || count($feed->processors_item) == 0) { if (!$cron) { drupal_set_message(t("This feed (%url) doesn't have any item processor. It's not possible to refresh the feed.", array('%url' => $feed->url)), "error"); drupal_goto($goto); } return; } if (!is_array($feed->processors_feed) || count($feed->processors_feed) == 0) { if (!$cron) { drupal_set_message(t("This feed (%url) doesn't have any feed processor. It's not possible to refresh the feed.", array('%url' => $feed->url)), "error"); drupal_goto($goto); } return; } $processors_item = $feed->processors_item; $processors_feed = $feed->processors_feed; // Force the processors to delete old items and determine the max. create elements $max_new_items = variable_get('feedapi_refresh_once', 10); foreach ($feed->processors_feed as $processor) { module_invoke($processor, 'feedapi_expire', $feed, $cron === TRUE ? FALSE : TRUE); } $feed = _feedapi_call_parsers($feed, $feed->parsers); $feed->processors_item = $processors_item; $feed->processors_feed = $processors_feed; $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_item as $processor) { if (!module_invoke($processor, 'feedapi_item_unique', $item, $feed->fid)) { module_invoke($processor, 'feedapi_item_update', $item, $feed->fid); $is_updated = TRUE; } else { module_invoke($processor, 'feedapi_item_save', $item, $feed->fid); $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 fid = %d", time(), $feed->fid); module_invoke_all('feedapi_after_refresh', $feed); if (!$cron) { drupal_set_message(t("%new feed item(s) are created and %updated feed items are updated.", array("%new" => $new, "%updated" => $updated))); drupal_goto($goto); } break; case 'purge': feedapi_invoke_feedapi("load", $feed, "info"); // Delete items from the processors foreach ($feed->items as $item) { foreach($feed->processors_item as $processor) { module_invoke($processor, 'feedapi_item_delete', $item); } } // Delete feed from the processors foreach($feed->processors_feed as $processor) { module_invoke($processor, 'feedapi_delete', $feed); } // Delete core data db_query("DELETE FROM {feedapi} WHERE fid = %d", $feed->fid); module_invoke_all('feedapi_after_purge', $feed); drupal_goto("admin/content/feed"); break; case 'save': db_query("INSERT INTO {feedapi} ( url, feed_type, processors_item, processors_feed, parsers, refresh, checked, uid) VALUES ('%s', '%s', '%s', '%s', '%s', %d, %d, %d)", $feed->url, $feed->feed_type, serialize($feed->processors_item), serialize($feed->processors_feed), serialize($feed->parsers), $feed->refresh, 0, // Means that the feed have never been refreshed yet $user->uid ); $feed->fid = db_result(db_query("SELECT fid FROM {feedapi} WHERE url = '%s'", $feed->url)); // Parse the feed with the parsers $title = $feed->title; $description = $feed->description; $feed = _feedapi_call_parsers($feed, $feed->parsers); // If the user provided a title and a description, drop the parsers' extracted $feed->title = !empty($title) ? $title : $feed->title; $feed->description = !empty($description) ? $description : $feed->description; foreach ($feed->processors_feed as $processor) { $feed = module_invoke($processor, 'feedapi_save', $feed); } module_invoke_all('feedapi_after_save', $feed); drupal_set_message(t('The %feed feed (%url) was saved successfully.', array('%feed' => $feed->title, '%url' => $feed->url))); // Tell the user if the feed is currently not usable if (strlen($feed->feed_type) < 1 || !module_exists($feed->parsers['primary']) || !module_exists($feed->processors_feed[0]) || !module_exists($feed->processors_item[0])) { drupal_set_message(t('The feed is not usable now, because there is no suitable processors and parser in the system')); } break; case 'update': // Store the common things db_query("UPDATE {feedapi} SET feed_type = '%s', url = '%s', processors_item = '%s', processors_feed = '%s', parsers = '%s', refresh = %d, checked = %d, uid = %d WHERE fid = %d", $feed->feed_type, $feed->url, serialize($feed->processors_item), serialize($feed->processors_feed), serialize($feed->parsers), $feed->refresh, $feed->checked, $user->uid, $feed->fid ); // Parse the feed with the parsers $title = $feed->title; $description = $feed->description; $feed = _feedapi_call_parsers($feed, $feed->parsers); // If the user provided a title and a description, drop the parsers' extracted $feed->title = !empty($title) ? $title : $feed->title; $feed->description = !empty($description) ? $description : $feed->description; // Call the enabled feed processors foreach ($feed->processors_feed as $processor) { // Check if the feed processor has this feed or not $feed_try_load = new stdClass(); $feed_try_load->options = new stdClass(); $feed_try_load->fid = $feed->fid; if (module_invoke($processor, 'feedapi_load', $feed_try_load) === FALSE) { $feed = module_invoke($processor, 'feedapi_save', $feed); } else { $feed = module_invoke($processor, 'feedapi_update', $feed); } } module_invoke_all('feedapi_after_update', $feed); drupal_set_message(t('The %feed feed (%url) was updated successfully.', array('%feed' => $feed->title, '%url' => $feed->url))); break; } } /** * Implementation of hook_cron(). */ function feedapi_cron() { $result = db_query_range("SELECT fid FROM {feedapi} ORDER BY checked ASC", 0, variable_get('feedapi_cron_max', 5)); while ($fid = db_result($result, $row++)) { $feed = new stdClass(); $feed->fid = $fid; feedapi_invoke_feedapi('load', $feed); feedapi_invoke_feedapi('refresh', $feed, TRUE); } } /** * Menu callback -- ask for confirmation of feed deletion */ function feedapi_delete_confirm($feed) { $form['fid'] = array('#type' => 'value', '#value' => $feed->fid); return confirm_form($form, t('Are you sure you want to delete %title?', array('%title' => $feed->title)), 'admin/content/feed', t('This action cannot be undone.'), t('Delete'), t('Cancel')); } /** * Execute feed deletion */ function feedapi_delete_confirm_submit($form_id, $form_values) { if ($form_values['confirm']) { $feed = new stdClass(); $feed->fid = $form_values['fid']; feedapi_invoke_feedapi("purge", $feed); } } /** * 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 fid from {feedapi}"); while ($fid = db_result($result, $row++)) { $commands = array(l(t('Delete'), 'feed/'. $fid . '/delete'), l(t('Refresh'), 'feed/'. $fid . '/refresh'), l(t('Edit'), 'feed/'. $fid . '/edit'), ); $ext_commands = module_invoke_all('feedapi_edit_option', $fid); if (count($ext_commands) > 0) { foreach ($ext_commands as $command) { $commands[] = l($command['name'], $command['link']); } } $feed = new stdClass(); $feed->fid = $fid; feedapi_invoke_feedapi("load", $feed); $rows[] = array($feed->url, format_interval($feed->refresh), $feed->checked == 0 ? t('Never') : format_interval(time() - $feed->checked) ." ". t("ago"), $feed->title, theme_item_list($commands) ); } return theme_table($header, $rows); } /** * Provide UI for edit a feed */ function feedapi_edit_page($feed) { $form = _feedapi_common_form($feed); foreach (array('fid', 'feed_type', 'checked') as $value) { $form[$value] = array('#type' => 'hidden', '#value' => $feed->{$value}); } // Select from the parsers and the processors $all = strlen($feed->feed_type) < 1 ? TRUE : FALSE; $form['processors_feed'] = array('#type' => 'checkboxes', '#options' => _feedapi_suitable_feed_processors($feed->feed_type, $all), '#default_value' => $feed->processors_feed, '#title' => t('Feed processor'), ); $form['processors_item'] = array('#type' => 'checkboxes', '#options' => _feedapi_suitable_item_processors($feed->feed_type, $all), '#default_value' => $feed->processors_item, '#title' => t('Feed item processor'), ); $parsers = _feedapi_suitable_parsers($feed->feed_type, $all); $form['primary'] = array('#type' => 'radios', '#title' => t('Primary parser'), '#options' => $parsers, '#default_value' => $feed->parsers['primary'], '#required' => TRUE, ); $form['secondary'] = array('#type' => 'checkboxes', '#title' => t('Secondary parsers'), '#options' => $parsers, '#default_value' => $feed->parsers['secondary'], ); $form['processors_settings'] = array('#type' => 'fieldset', '#title' => t('The currently enabled processors\' settings'), '#collapsible' => TRUE, '#collapsed' => FALSE, ); $form['submit'] = array('#type' => 'submit', '#value' => t('Submit')); return $form; } /** * Validate as the submission */ function feedapi_edit_page_validate($form_id, $form_values) { $feed = new stdClass(); $feed->url = $form_values['url']; foreach ($form_values['processors_feed'] as $processor) { $feed = module_invoke($processor, 'feedapi_validate', $form_id, $form_values, $feed); } // Check if the selected parser-processor configuration has a common type $type = module_invoke($form_values['primary'], 'feedapi_type'); $type = is_array($type) ? $type : array(); foreach ($form_values['processors_item'] as $processor => $turned) { if ($turned && !empty($processor)) { $type = array_intersect($type, module_invoke($processor, 'feedapi_type')); } } foreach ($form_values['processors_feed'] as $processor => $turned) { if ($turned && !empty($processor)) { $type = array_intersect($type, module_invoke($processor, 'feedapi_type')); } } if (count($type) == 0) { form_set_error('feedapi', t('You have selected an invalid parser-processor configuration.'), 'error'); } } /** * Construct parsers and processors data structures and call update */ function feedapi_edit_page_submit($form_id, $form_values) { $feed = new stdClass(); $feed->parsers = array(); $feed->parsers['primary'] = $form_values['primary']; $feed->parsers['secondary'] = array(); foreach ($form_values['secondary'] as $parser => $turned) { if ($turned) { $feed->parsers['secondary'][] = $parser; } } if (strlen($form_values['feed_type']) < 1) { // Check the feed against the main parser to get the type $form_values['feed_type'] = module_invoke($feed->parsers['primary'], 'feed_compatible', $form_values['url']); } $feed->processors_item = array_values($form_values['processors_item']); $feed->processors_feed = array_values($form_values['processors_feed']); $feed = (object) array_merge($form_values, (array) $feed); feedapi_invoke_feedapi("update", $feed); drupal_goto('admin/content/feed'); } /** * Provide a UI for create a feed */ function feedapi_add_page() { $form = _feedapi_common_form(); $form['advanced'] = array('#type' => 'checkbox', '#title' => t('Show me the advanced parser/processor configuration'), '#default_value' => FALSE, '#description' => t('After the feed will be created, you will be able to edit the assigned parsers and processors.'), ); $form['submit'] = array('#type' => 'submit', '#value' => t('Submit')); return $form; } /** * Let the feed processor allow or reject the given URL */ function feedapi_add_page_validate($form_id, $form_values) { $feed = new stdClass(); $feed->url = $form_values['url']; $assigned = _feedapi_assign_parser_processor($feed->url); $feed = module_invoke($assigned['processor_feed'], 'feedapi_validate', $form_id, $form_values, $feed); if (db_result(db_query("SELECT fid FROM {feedapi} WHERE url = '%s'", $feed->url))) { form_set_error('feedapi', t('The feed URL provided already exists. Please use a different URL.')); } } /** * Create the feed and invoke the the assigned processor */ function feedapi_add_page_submit($form_id, $form_values) { // Assign one parser and one processor to the URL $assigned = _feedapi_assign_parser_processor($form_values['url']); $feed = new stdClass(); $feed = (object) array_merge($form_values, (array) $feed); $feed->parsers = array(); // We store it in such structures because the user will be able to assign more parsers and processors to one feed later $feed->parsers['primary'] = $assigned['parser']; $feed->parsers['secondary'] = array(); $feed->feed_type = $assigned['type']; $feed->processors_item = array($assigned['processor_item']); $feed->processors_feed = array($assigned['processor_feed']); // Store the common things feedapi_invoke_feedapi('save', $feed); drupal_goto(($form_values['advanced'] == TRUE) ? 'feed/'. $feed->fid. '/edit' : drupal_init_path()); } /** * 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', '