Index: modules/aggregator/aggregator.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.api.php,v retrieving revision 1.4 diff -u -r1.4 aggregator.api.php --- modules/aggregator/aggregator.api.php 24 May 2009 17:39:30 -0000 1.4 +++ modules/aggregator/aggregator.api.php 21 Jun 2009 15:44:28 -0000 @@ -20,8 +20,8 @@ * finally, it is passed to all active processors which manipulate or store the * data. * - * Modules that define this hook can be set as active fetcher on - * admin/settings/aggregator. Only one fetcher can be active at a time. + * Modules that define this hook can be set as active fetcher on the content + * type form of the feed. Only one fetcher can be active at a time. * * @param $feed * The $feed object that describes the resource to be downloaded. @@ -42,10 +42,10 @@ * Implement this hook to expose the title and a short description of your * fetcher. * - * The title and the description provided are shown on - * admin/settings/aggregator among other places. Use as title the human - * readable name of the fetcher and as description a brief (40 to 80 characters) - * explanation of the fetcher's functionality. + * The title and the description provided are shown for example on content type + * edit forms. Use as title the natural name of the fetcher and as + * description a brief (40 to 80 characters) explanation of the fetcher's + * functionality. * * This hook is only called if your module implements hook_aggregator_fetch(). * If this hook is not implemented aggregator will use your module's file name @@ -74,8 +74,8 @@ * finally, it is passed to all active processors which manipulate or store the * data. * - * Modules that define this hook can be set as active parser on - * admin/settings/aggregator. Only one parser can be active at a time. + * Modules that define this hook can be set as active parser on the content + * type form of the feed. Only one parser can be active at a time. * * @param $feed * The $feed object that describes the resource to be parsed. @@ -108,9 +108,9 @@ * Implement this hook to expose the title and a short description of your * parser. * - * The title and the description provided are shown on admin/settings/aggregator - * among other places. Use as title the human readable name of the parser and - * as description a brief (40 to 80 characters) explanation of the parser's + * The title and the description provided are shown for example on content type + * edit forms. Use as title the natural name of the parser and as + * description a brief (40 to 80 characters) explanation of the parser's * functionality. * * This hook is only called if your module implements hook_aggregator_parse(). @@ -140,8 +140,8 @@ * finally, it is passed to all active processors which manipulate or store the * data. * - * Modules that define this hook can be activated as processor on - * admin/settings/aggregator. + * Modules that define this hook can be activated as processor on the content + * type form of the feed * * @param $feed * The $feed object that describes the resource to be processed. $feed->items @@ -166,10 +166,9 @@ * Implement this hook to expose the title and a short description of your * processor. * - * The title and the description provided are shown most importantly on - * admin/settings/aggregator. Use as title the natural name of the processor - * and as description a brief (40 to 80 characters) explanation of the - * functionality. + * The title and the description provided are shown for example on content type + * edit forms. Use as title the natural name of the processor and as + * description a brief (40 to 80 characters) explanation of the functionality. * * This hook is only called if your module implements * hook_aggregator_process(). If this hook is not implemented aggregator Index: modules/aggregator/aggregator.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.admin.inc,v retrieving revision 1.31 diff -u -r1.31 aggregator.admin.inc --- modules/aggregator/aggregator.admin.inc 28 May 2009 09:30:58 -0000 1.31 +++ modules/aggregator/aggregator.admin.inc 21 Jun 2009 15:44:28 -0000 @@ -7,6 +7,36 @@ */ /** + * Implementation of hook_requirements(). + */ +function aggregator_requirements($phase) { + $requirements = array(); + + if ($phase == 'runtime') { + + // Check whether there are aggregator-enabled content types; help + // the user to aggregator-enable a content type if not. + $requirements['aggregator_types'] = array( + 'title' => t('Aggregator content types'), + ); + if ($types = aggregator_get_enabled_content_types()) { + $requirements['aggregator_types'] += array( + 'value' => implode(', ', $types), + 'severity' => REQUIREMENT_OK, + ); + } + else { + $requirements['aggregator_types'] += array( + 'value' => t('No aggregator-enabled content types'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t('To aggregate feeds on your site, you have to enable at least one content type to do so. Edit !content_types and check "Feed content type" in the Aggregator field.', array('!content_types' => l(t('one of the content types'), 'admin/build/types'))), + ); + } + } + return $requirements; +} + +/** * Menu callback; displays the aggregator administration page. */ function aggregator_admin_overview() { @@ -20,14 +50,22 @@ * The page HTML. */ function aggregator_view() { - $result = db_query('SELECT f.*, COUNT(i.iid) AS items FROM {aggregator_feed} f LEFT JOIN {aggregator_item} i ON f.fid = i.fid GROUP BY f.fid, f.title, f.url, f.refresh, f.checked, f.link, f.description, f.hash, f.etag, f.modified, f.image, f.block ORDER BY f.title'); + $result = db_query('SELECT n.title, f.*, COUNT(i.iid) AS items FROM {aggregator_feed} f INNER JOIN {node} n ON n.vid = f.vid LEFT JOIN {aggregator_item} i ON f.nid = i.nid GROUP BY f.nid, n.title, f.url, f.refresh, f.checked, f.link, f.hash, f.etag, f.modified, f.image, f.block ORDER BY n.title'); $output = '
' . t('Thousands of sites (particularly news sites and blogs) publish their latest headlines and posts in feeds, using a number of standardized XML-based formats. Formats supported by the aggregator include RSS, RDF, and Atom.', array('@rss' => 'http://cyber.law.harvard.edu/rss/', '@rdf' => 'http://www.w3.org/RDF/', '@atom' => 'http://www.atomenabled.org')) . '
'; - $output .= '' . t('Current feeds are listed below, and new feeds may be added. For each feed or feed category, the latest items block may be enabled at the blocks administration page.', array('@addfeed' => url('admin/content/aggregator/add/feed'), '@block' => url('admin/build/block'))) . '
'; + $output .= '' . t('Current feeds are listed below, and new feeds may be added. For each feed or feed category, the latest items block may be enabled at the blocks administration page.', array('@addfeed' => url('node/add'), '@block' => url('admin/build/block'))) . '
'; return $output; - case 'admin/content/aggregator/add/feed': - return '' . t('Add a feed in RSS, RDF or Atom format. A feed may only have one entry.') . '
'; case 'admin/content/aggregator/add/category': return '' . t('Categories allow feed items from different feeds to be grouped together. For example, several sport-related feeds may belong to a category named Sports. Feed items may be grouped automatically (by selecting a category when creating or editing a feed) or manually (via the Categorize page available from feed item listings). Each category provides its own feed page and block.') . '
'; case 'admin/content/aggregator/add/opml': @@ -48,8 +46,12 @@ 'arguments' => array('form' => NULL), 'file' => 'aggregator.pages.inc', ), + 'aggregator_feed_info' => array( + 'arguments' => array('feed_node' => NULL), + 'template' => 'aggregator-feed-info', + ), 'aggregator_feed_source' => array( - 'arguments' => array('feed' => NULL), + 'arguments' => array('feed_node' => NULL), 'file' => 'aggregator.pages.inc', 'template' => 'aggregator-feed-source', ), @@ -86,20 +88,30 @@ * Implement hook_menu(). */ function aggregator_menu() { + $items['node/%node/update'] = array( + 'title' => 'Update feed', + 'description' => 'Check feed for new items', + 'page callback' => 'aggregator_admin_refresh_feed', + 'page arguments' => array(1), + 'access callback' => 'aggregator_access_feed_options', + 'access arguments' => array(1), + 'type' => MENU_LOCAL_TASK, + ); + $items['node/%node/remove-items'] = array( + 'title' => 'Remove items', + 'description' => 'Remove all items', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('aggregator_admin_remove_feed', 1), + 'access callback' => 'aggregator_access_feed_options', + 'access arguments' => array(1), + 'type' => MENU_LOCAL_TASK, + ); $items['admin/content/aggregator'] = array( 'title' => 'Feed aggregator', 'description' => "Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized.", 'page callback' => 'aggregator_admin_overview', 'access arguments' => array('administer news feeds'), ); - $items['admin/content/aggregator/add/feed'] = array( - 'title' => 'Add feed', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('aggregator_form_feed'), - 'access arguments' => array('administer news feeds'), - 'type' => MENU_LOCAL_TASK, - 'parent' => 'admin/content/aggregator', - ); $items['admin/content/aggregator/add/category'] = array( 'title' => 'Add category', 'page callback' => 'drupal_get_form', @@ -116,32 +128,11 @@ 'type' => MENU_LOCAL_TASK, 'parent' => 'admin/content/aggregator', ); - $items['admin/content/aggregator/remove/%aggregator_feed'] = array( - 'title' => 'Remove items', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('aggregator_admin_remove_feed', 4), - 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, - ); - $items['admin/content/aggregator/update/%aggregator_feed'] = array( - 'title' => 'Update items', - 'page callback' => 'aggregator_admin_refresh_feed', - 'page arguments' => array(4), - 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, - ); $items['admin/content/aggregator/list'] = array( 'title' => 'List', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); - $items['admin/settings/aggregator'] = array( - 'title' => 'Aggregator', - 'description' => 'Configure the behavior of the feed aggregator, including when to discard feed items and how to present feed items and categories.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('aggregator_admin_form'), - 'access arguments' => array('administer news feeds'), - ); $items['aggregator'] = array( 'title' => 'Feed aggregator', 'page callback' => 'aggregator_page_last', @@ -198,39 +189,24 @@ 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); - $items['aggregator/sources/%aggregator_feed'] = array( + $items['aggregator/sources/%node'] = array( 'page callback' => 'aggregator_page_source', 'page arguments' => array(2), 'access arguments' => array('access news feeds'), 'type' => MENU_CALLBACK, ); - $items['aggregator/sources/%aggregator_feed/view'] = array( + $items['aggregator/sources/%node/view'] = array( 'title' => 'View', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); - $items['aggregator/sources/%aggregator_feed/categorize'] = array( + $items['aggregator/sources/%node/categorize'] = array( 'title' => 'Categorize', 'page callback' => 'drupal_get_form', 'page arguments' => array('aggregator_page_source', 2), 'access arguments' => array('administer news feeds'), 'type' => MENU_LOCAL_TASK, ); - $items['aggregator/sources/%aggregator_feed/configure'] = array( - 'title' => 'Configure', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('aggregator_form_feed', 2), - 'access arguments' => array('administer news feeds'), - 'type' => MENU_LOCAL_TASK, - 'weight' => 1, - ); - $items['admin/content/aggregator/edit/feed/%aggregator_feed'] = array( - 'title' => 'Edit feed', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('aggregator_form_feed', 5), - 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, - ); $items['admin/content/aggregator/edit/category/%aggregator_category'] = array( 'title' => 'Edit category', 'page callback' => 'drupal_get_form', @@ -291,7 +267,7 @@ * Checks news feeds for updates once their refresh interval has elapsed. */ function aggregator_cron() { - $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < :time AND refresh != :never', array( + $result = db_query('SELECT n.title, n.type, f.* FROM {node} n INNER JOIN {aggregator_feed} f ON f.vid = n.vid WHERE f.checked + f.refresh < :time AND f.refresh <> :never', array( ':time' => REQUEST_TIME, ':never' => AGGREGATOR_CLEAR_NEVER )); @@ -301,6 +277,204 @@ } /** + * Implementation of hook_form_alter(). + * + * Add aggregator related options to node forms of feed-enabled content types. + */ +function aggregator_form_alter(&$form, $form_state, $form_id) { + if ($form['#id'] == 'node-form') { + if (variable_get('aggregator_enabled_' . $form['type']['#value'], FALSE)) { + if (!isset($form['#node']->feed)) { + $form['#node']->feed = new stdClass(); + } + if (is_array($form['#node']->feed)) { + $form['#node']->feed = (object) $form['#node']->feed; + } + $form['feed'] = array( + '#type' => 'fieldset', + '#title' => t('Feed'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#tree' => TRUE, + '#weight' => -2, + ); + $form['feed']['url'] = array( + '#type' => 'textfield', + '#title' => t('URL'), + '#description' => t('Enter the full URL of the feed you would like to add.'), + '#default_value' => empty($form['#node']->feed->url) ? '' : $form['#node']->feed->url, + '#required' => TRUE, + ); + // Update interval and news items in blocks only configurable by administrators. + if (user_access('administer news feeds')) { + $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval'); + $form['feed']['refresh'] = array( + '#type' => 'select', + '#title' => t('Update interval'), + '#default_value' => empty($form['#node']->feed->refresh) ? 1800 : $form['#node']->feed->refresh, + '#options' => $period, + '#description' => t('The length of time between feed updates. Requires a correctly configured cron maintenance task.', array('@cron' => url('admin/reports/status'))), + ); + } + else { + $form['feed']['refresh'] = array( + '#type' => 'value', + '#value' => empty($form['#node']->feed->refresh) ? 1800 : $form['#node']->feed->refresh, + ); + } + $processors = variable_get('aggregator_processors_' . $form['type']['#value'], array('aggregator')); + // If aggregator-native processor is enabled, add processor specific form elements. + if (in_array('aggregator', $processors)) { + if (drupal_function_exists('aggregator_processor_node_form')) { + aggregator_processor_node_form($form, $form_state, $form_id); + } + } + } + } +} + +/** + * Implementation of hook_node_load(). + */ +function aggregator_node_load($nodes, $types) { + $nids = array(); + foreach ($nodes as $node) { + if (variable_get('aggregator_enabled_' . $node->type, FALSE)) { + $nids[$node->nid] = $node->nid; + } + } + if (count($nids)) { + $result = db_query('SELECT n.title, n.type, f.* FROM {node} n INNER JOIN {aggregator_feed} f ON f.vid = n.vid WHERE n.nid IN (:nids)', array(':nids' => $nids)); + foreach ($result as $record) { + $nodes[$record->nid]->feed = $record; + } + } +} + +/** + * Implementation of hook_node_validate(). + */ +function aggregator_node_validate($node) { + if (isset($node->feed['url'])) { + if (!valid_url($node->feed['url'], TRUE)) { + form_set_error('feed][url', t('The URL %url is invalid. Please enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $node->feed['url']))); + } + // Check for duplicate titles. + if (empty($node->nid)) { + $result = db_query("SELECT n.title, f.url FROM {node} n INNER JOIN {aggregator_feed} f ON f.vid = n.vid WHERE n.title = :title OR f.url = :url", array(':title' => $node->title, ':url' => $node->feed['url'])); + } + else { + $result = db_query("SELECT n.title, f.url FROM {node} n INNER JOIN {aggregator_feed} f ON f.vid = n.vid WHERE (n.title = :title OR f.url = :url) AND f.nid <> :nid", array(':title' => $node->title, ':url' => $node->feed['url'], ':nid' => $node->nid)); + } + foreach ($result as $feed) { + if (strcasecmp($feed->title, $node->title) == 0) { + form_set_error('title', t('A feed named %feed already exists. Please enter a unique title.', array('%feed' => $node->title))); + } + if (strcasecmp($feed->url, $node->feed['url']) == 0) { + form_set_error('url', t('A feed with this URL %url already exists. Please enter a unique URL.', array('%url' => $node->feed['url']))); + } + } + } +} + +/** + * Implementation of hook_node_insert(). + */ +function aggregator_node_insert($node) { + if (variable_get('aggregator_enabled_' . $node->type, FALSE)) { + $node->feed['nid'] = $node->nid; + $node->feed['vid'] = $node->vid; + aggregator_save_feed($node->feed); + } +} + +/** + * Implementation of hook_node_update(). + */ +function aggregator_node_update($node) { + if (variable_get('aggregator_enabled_' . $node->type, FALSE)) { + // If reverting to a previous version, load data at old_vid. + if (is_object($node->feed) && isset($node->old_vid)) { + $node->feed = db_query("SELECT * FROM {aggregator_feed} WHERE vid = :old", array(':old' => $node->old_vid))->fetchAssoc(); + if (drupal_function_exists('aggregator_processor_save_feed_category')) { + $node->feed['category'] = db_query("SELECT cid FROM {aggregator_category_feed} WHERE vid = :old", array(':old' => $node->old_vid))->fetchAssoc(); + } + } + $node->feed['vid'] = $node->vid; + $node->feed['nid'] = $node->nid; + aggregator_save_feed($node->feed); + } +} + +/** + * Implementation of hook_node_delete(). + */ +function aggregator_node_delete($node) { + if (variable_get('aggregator_enabled_' . $node->type, FALSE)) { + $iids = db_query('SELECT iid FROM {aggregator_item} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol(); + if ($iids) { + db_delete('aggregator_category_item') + ->condition('iid', $iids, 'IN') + ->execute(); + } + db_delete('aggregator_feed')-> + condition('nid', $node->nid) + ->execute(); + db_delete('aggregator_category_feed') + ->condition('nid', $node->nid) + ->execute(); + db_delete('aggregator_item') + ->condition('nid', $node->nid) + ->execute(); + // Make sure there is no active block for this feed. + db_delete('block') + ->condition('module', 'aggregator') + ->condition('delta', 'feed-' . $node->nid) + ->execute(); + } +} + +/** + * Implementation of hook_node_delete_revision(). + */ +function hook_node_delete_revision($node) { + db_delete('aggregator_feed') + ->condition('vid', $node->vid) + ->execute(); + db_delete('aggregator_category_feed') + ->condition('vid', $node->vid) + ->execute(); +} + +/** + * An implementation of hook_node_view(). + */ +function aggregator_node_view($node, $teaser) { + if (variable_get('aggregator_enabled_' . $node->type, FALSE)) { + $processors = variable_get('aggregator_processors_' . $node->type, array('aggregator')); + if (in_array('aggregator', $processors)) { + $links = array(); + $links['aggregator_items'] = array( + 'title' => t('Feed items'), + 'href' => 'aggregator/sources/' . $node->nid, + ); + $node->content['links']['aggregator'] = array( + '#type' => 'node_links', + '#value' => $links, + ); + } + // On a full node view, add aggregator feed info. + if (!$teaser) { + $items = array(); + $node->content['aggregator_feed_info'] = array( + '#weight' => -1, + '#markup' => theme('aggregator_feed_info', $node), + ); + } + } +} + +/** * Implement hook_block_list(). */ function aggregator_block_list() { @@ -309,9 +483,9 @@ foreach ($result as $category) { $block['category-' . $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title)); } - $result = db_query('SELECT fid, title FROM {aggregator_feed} WHERE block <> 0 ORDER BY fid'); + $result = db_query('SELECT n.vid, n.title FROM {node} n JOIN {aggregator_feed} f ON f.vid = n.vid WHERE f.block <> 0 ORDER BY vid'); foreach ($result as $feed) { - $block['feed-' . $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title)); + $block['feed-' . $feed->vid]['info'] = t('!title feed latest items', array('!title' => $feed->title)); } return $block; } @@ -357,10 +531,10 @@ list($type, $id) = explode('-', $delta); switch ($type) { case 'feed': - if ($feed = db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE block <> 0 AND fid = :fid', array(':fid' => $id))->fetchObject()) { + if ($feed = db_query('SELECT n.nid, n.title, f.block FROM {node} n INNER JOIN {aggregator_feed} f ON f.vid = n.vid WHERE f.block <> 0 AND f.vid = :vid', array(':vid' => $id))->fetchObject()) { $block['subject'] = check_plain($feed->title); - $result = db_query_range("SELECT * FROM {aggregator_item} WHERE fid = :fid ORDER BY timestamp DESC, iid DESC", array(':fid' => $id), 0, $feed->block); - $read_more = theme('more_link', url('aggregator/sources/' . $feed->fid), t("View this feed's recent news.")); + $result = db_query_range("SELECT * FROM {aggregator_item} WHERE nid = :nid ORDER BY timestamp DESC, iid DESC", array(':nid' => $id), 0, $feed->block); + $read_more = theme('more_link', url('aggregator/sources/' . $feed->nid), t("View this feed's recent news.")); } break; @@ -435,74 +609,33 @@ } /** - * Add/edit/delete an aggregator feed. + * Save an aggregator feed. * - * @param $edit - * An associative array describing the feed to be added/edited/deleted. + * @param $feed + * An object or an associative array describing the feed to be + * inserted/updated. */ -function aggregator_save_feed($edit) { - if (!empty($edit['fid'])) { - // An existing feed is being modified, delete the category listings. - db_delete('aggregator_category_feed') - ->condition('fid', $edit['fid']) - ->execute(); +function aggregator_save_feed($feed) { + if (is_object($feed)) { + $feed = (array)$feed; } - if (!empty($edit['fid']) && !empty($edit['title'])) { - db_update('aggregator_feed') - ->condition('fid', $edit['fid']) - ->fields(array( - 'title' => $edit['title'], - 'url' => $edit['url'], - 'refresh' => $edit['refresh'], - 'block' => $edit['block'], - )) - ->execute(); - } - elseif (!empty($edit['fid'])) { - $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $edit['fid']))->fetchCol(); - if ($iids) { - db_delete('aggregator_category_item') - ->condition('iid', $iids, 'IN') - ->execute(); + + $keys = drupal_schema_fields_sql('aggregator_feed'); + $fields = array(); + foreach ($keys as $key) { + if (isset($feed[$key])) { + $fields[$key] = $feed[$key]; } - db_delete('aggregator_feed')-> - condition('fid', $edit['fid']) - ->execute(); - db_delete('aggregator_item') - ->condition('fid', $edit['fid']) - ->execute(); - // Make sure there is no active block for this feed. - db_delete('block') - ->condition('module', 'aggregator') - ->condition('delta', 'feed-' . $edit['fid']) - ->execute(); } - elseif (!empty($edit['title'])) { - $edit['fid'] = db_insert('aggregator_feed') - ->fields(array( - 'title' => $edit['title'], - 'url' => $edit['url'], - 'refresh' => $edit['refresh'], - 'block' => $edit['block'], - 'description' => '', - 'image' => '', - )) + if (count($fields) && isset($fields['vid']) && isset($fields['nid'])) { + db_merge('aggregator_feed') + ->key(array('vid' => $fields['vid'])) + ->fields($fields) ->execute(); - } - if (!empty($edit['title'])) { - // The feed is being saved, save the categories as well. - if (!empty($edit['category'])) { - foreach ($edit['category'] as $cid => $value) { - if ($value) { - db_merge('aggregator_category_feed') - ->key(array('fid' => $edit['fid'])) - ->fields(array( - 'cid' => $cid, - )) - ->execute(); - } - } + if (isset($feed['category'])) { + if (drupal_function_exists('aggregator_processor_save_feed_category')) { + aggregator_processor_save_feed_category($feed); } } } @@ -510,22 +643,20 @@ /** * Removes all items from a feed. * - * @param $feed - * An object describing the feed to be cleared. + * @param $node + * An object describing the feed node to be cleared. */ -function aggregator_remove($feed) { +function aggregator_remove($node) { // Call hook_aggregator_remove() on all modules. - module_invoke_all('aggregator_remove', $feed); + module_invoke_all('aggregator_remove', $node->feed); // Reset feed. - db_merge('aggregator_feed') - ->key(array('fid' => $feed->fid)) + db_update('aggregator_feed') + ->condition('nid', $node->nid) ->fields(array( 'checked' => 0, 'hash' => '', 'etag' => '', 'modified' => 0, - 'description' => $feed->description, - 'image' => $feed->image, )) ->execute(); } @@ -538,17 +669,17 @@ */ function aggregator_refresh($feed) { // Fetch the feed. - $fetcher = variable_get('aggregator_fetcher', 'aggregator'); + $fetcher = variable_get('aggregator_fetcher_' . $feed->type, 'aggregator'); module_invoke($fetcher, 'aggregator_fetch', $feed); if ($feed->source_string !== FALSE) { // Parse the feed. - $parser = variable_get('aggregator_parser', 'aggregator'); + $parser = variable_get('aggregator_parser_' . $feed->type, 'aggregator'); module_invoke($parser, 'aggregator_parse', $feed); // If there are items on the feed, let all enabled processors do their work on it. if (@count($feed->items)) { - $processors = variable_get('aggregator_processors', array('aggregator')); + $processors = variable_get('aggregator_processors_' . $feed->type, array('aggregator')); foreach ($processors as $processor) { module_invoke($processor, 'aggregator_process', $feed); } @@ -561,23 +692,6 @@ } /** - * Load an aggregator feed. - * - * @param $fid - * The feed id. - * @return - * An object describing the feed. - */ -function aggregator_feed_load($fid) { - $feeds = &drupal_static(__FUNCTION__); - if (!isset($feeds[$fid])) { - $feeds[$fid] = db_query('SELECT * FROM {aggregator_feed} WHERE fid = :fid', array(':fid' => $fid))->fetchObject(); - } - - return $feeds[$fid]; -} - -/** * Load an aggregator category. * * @param $cid @@ -595,6 +709,22 @@ } /** + * Access callback for aggregator_menu(). + * + * @param $node + * Node object. + * @return + * TRUE, if given feed is an object and user has administer news feeds + * permissions. FALSE otherwise. + */ +function aggregator_access_feed_options($node) { + if (isset($node->feed) && is_object($node->feed) && user_access('administer news feeds')) { + return TRUE; + } + return FALSE; +} + +/** * Format an individual feed item for display in the block. * * @param $item @@ -606,10 +736,8 @@ * @ingroup themeable */ function theme_aggregator_block_item($item, $feed = 0) { - // Display the external link to the item. return '' . check_plain($item->title) . "\n"; - } /** @@ -660,6 +788,19 @@ } /** + * Get all aggregator enabled content types. + */ +function aggregator_get_enabled_content_types() { + $type_names = node_type_get_names(); + foreach ($type_names as $type => $name) { + if (!variable_get('aggregator_enabled_' . $type, FALSE)) { + unset($type_names[$type]); + } + } + return $type_names; +} + +/** * Helper function for drupal_map_assoc. * * @param $count Index: modules/aggregator/aggregator.parser.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.parser.inc,v retrieving revision 1.3 diff -u -r1.3 aggregator.parser.inc --- modules/aggregator/aggregator.parser.inc 27 May 2009 18:33:54 -0000 1.3 +++ modules/aggregator/aggregator.parser.inc 21 Jun 2009 15:44:29 -0000 @@ -37,7 +37,7 @@ } if (!empty($image['link']) && !empty($image['url']) && !empty($image['title'])) { - $image = l(theme('image', $image['url'], $image['title']), $image['link'], array('html' => TRUE)); + $image = l(theme('image', $image['url'], t('Image of the feed'), $image['title'], NULL, FALSE), $image['link'], array('html' => TRUE)); } else { $image = ''; @@ -45,13 +45,12 @@ $etag = empty($feed->http_headers['ETag']) ? '' : $feed->http_headers['ETag']; // Update the feed data. - db_merge('aggregator_feed') - ->key(array('fid' => $feed->fid)) + db_update('aggregator_feed') + ->condition('vid', $feed->vid) ->fields(array( 'url' => $feed->url, 'checked' => REQUEST_TIME, 'link' => !empty($channel['link']) ? $channel['link'] : '', - 'description' => !empty($channel['description']) ? $channel['description'] : '', 'image' => $image, 'hash' => md5($feed->source_string), 'etag' => $etag, Index: modules/aggregator/aggregator.processor.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.processor.inc,v retrieving revision 1.9 diff -u -r1.9 aggregator.processor.inc --- modules/aggregator/aggregator.processor.inc 5 Jun 2009 05:28:28 -0000 1.9 +++ modules/aggregator/aggregator.processor.inc 21 Jun 2009 15:44:29 -0000 @@ -27,18 +27,18 @@ // we find a duplicate entry, we resolve it and pass along its ID is such // that we can update it if needed. if (!empty($item['guid'])) { - $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND guid = :guid", array(':fid' => $feed->fid, ':guid' => $item['guid']))->fetchObject(); + $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE nid = :nid AND guid = :guid", array(':nid' => $feed->nid, ':guid' => $item['guid']))->fetchObject(); } elseif ($item['link'] && $item['link'] != $feed->link && $item['link'] != $feed->url) { - $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND link = :link", array(':fid' => $feed->fid, ':link' => $item['link']))->fetchObject(); + $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE nid = :nid AND link = :link", array(':nid' => $feed->nid, ':link' => $item['link']))->fetchObject(); } else { - $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND title = :title", array(':fid' => $feed->fid, ':title' => $item['title']))->fetchObject(); + $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE nid = :nid AND title = :title", array(':nid' => $feed->nid, ':title' => $item['title']))->fetchObject(); } if (!$item['timestamp']) { $item['timestamp'] = isset($entry->timestamp) ? $entry->timestamp : REQUEST_TIME; } - aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid : ''), 'fid' => $feed->fid, 'timestamp' => $item['timestamp'], 'title' => $item['title'], 'link' => $item['link'], 'author' => $item['author'], 'description' => $item['description'], 'guid' => $item['guid'])); + aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid : ''), 'nid' => $feed->nid, 'vid' => $feed->vid, 'timestamp' => $item['timestamp'], 'title' => $item['title'], 'link' => $item['link'], 'author' => $item['author'], 'description' => $item['description'], 'guid' => $item['guid'])); } } } @@ -48,44 +48,46 @@ * Implement hook_aggregator_remove(). */ function aggregator_aggregator_remove($feed) { - $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchCol(); + $iids = db_query('SELECT iid FROM {aggregator_item} WHERE nid = :nid', array(':nid' => $feed->nid))->fetchCol(); if ($iids) { db_delete('aggregator_category_item') ->condition('iid', $iids, 'IN') ->execute(); } db_delete('aggregator_item') - ->condition('fid', $feed->fid) + ->condition('nid', $feed->nid) ->execute(); drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed->title))); } /** - * Implement hook_form_aggregator_admin_form_alter(). + * Add processor specific elements to the node type form. * - * Form alter aggregator module's own form to keep processor functionality - * separate from aggregator API functionality. + * Called from aggregator_form_node_type_form_alter(). We keep this functionality + * in aggregator.processor.inc because it is processor related. */ -function aggregator_form_aggregator_admin_form_alter(&$form, $form_state) { - if (in_array('aggregator', variable_get('aggregator_processors', array('aggregator')))) { +function aggregator_processor_node_type_form(&$form) { + $node_type = empty($form['#node_type']->type) ? '' : $form['#node_type']->type; + + if (in_array('aggregator', variable_get('aggregator_processors_' . $node_type, array('aggregator')))) { $info = module_invoke('aggregator', 'aggregator_process', 'info'); $items = array(0 => t('none')) + drupal_map_assoc(array(3, 5, 10, 15, 20, 25), '_aggregator_items'); $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval'); $period[AGGREGATOR_CLEAR_NEVER] = t('Never'); // Only wrap into a collapsible fieldset if there is a basic configuration. - if (isset($form['basic_conf'])) { - $form['modules']['aggregator'] = array( + if (isset($form['aggregator']['basic_conf'])) { + $form['aggregator']['processors']['aggregator'] = array( '#type' => 'fieldset', '#title' => t('Default processor settings'), '#description' => $info['description'], '#collapsible' => TRUE, - '#collapsed' => !in_array('aggregator', variable_get('aggregator_processors', array('aggregator'))), + '#collapsed' => !in_array('aggregator', variable_get('aggregator_processors_' . $node_type, array('aggregator'))), ); } else { - $form['modules']['aggregator'] = array(); + $form['aggregator']['processors']['aggregator'] = array(); } $form['modules']['aggregator']['aggregator_summary_items'] = array( @@ -96,18 +98,18 @@ '#description' => t('Number of feed items displayed in feed and category summary pages.'), ); - $form['modules']['aggregator']['aggregator_clear'] = array( + $form['aggregator']['processors']['aggregator']['aggregator_clear'] = array( '#type' => 'select', '#title' => t('Discard items older than'), - '#default_value' => variable_get('aggregator_clear', 9676800), + '#default_value' => variable_get('aggregator_clear_' . $node_type, 9676800), '#options' => $period, '#description' => t('The length of time to retain feed items before discarding. (Requires a correctly configured cron maintenance task.)', array('@cron' => url('admin/reports/status'))), ); - $form['modules']['aggregator']['aggregator_category_selector'] = array( + $form['aggregator']['processors']['aggregator']['aggregator_category_selector'] = array( '#type' => 'radios', '#title' => t('Category selection type'), - '#default_value' => variable_get('aggregator_category_selector', 'checkboxes'), + '#default_value' => variable_get('aggregator_category_selector_' . $node_type, 'checkboxes'), '#options' => array('checkboxes' => t('checkboxes'), 'select' => t('multiple selector')), '#description' => t('The type of category selection widget displayed on categorization pages. (For a small number of categories, checkboxes are easier to use, while a multiple selector works well with large numbers of categories.)'), @@ -131,6 +133,49 @@ } /** + * Add processor specific elements to the node form. + * + * Called from aggregator_form_alter(). Keep this functionality in + * aggregator.processor.inc because it is processor related. + */ +function aggregator_processor_node_form(&$form, $form_state, $form_id) { + // Handling of categories. + $options = array(); + $values = array(); + $categories = db_query('SELECT c.cid, c.title, f.nid FROM {aggregator_category} c LEFT JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.vid = :vid ORDER BY title', array(':vid' => empty($form['#node']->vid) ? NULL : $form['#node']->vid)); + foreach ($categories as $category) { + $options[$category->cid] = check_plain($category->title); + if ($category->nid) $values[] = $category->cid; + } + if ($options) { + $form['feed']['category'] = array( + '#type' => 'checkboxes', + '#title' => t('Categorize news items'), + '#default_value' => $values, + '#options' => $options, + '#description' => t('New feed items are automatically filed in the checked categories.'), + ); + } + // Update interval and news items in blocks only configurable by administrators. + if (user_access('administer news feeds')) { + $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval'); + $form['feed']['refresh'] = array( + '#type' => 'select', + '#title' => t('Update interval'), + '#default_value' => empty($form['#node']->feed->refresh) ? 1800 : $form['#node']->feed->refresh, + '#options' => $period, + '#description' => t('The length of time between feed updates. Requires a correctly configured cron maintenance task.', array('@cron' => url('admin/reports/status'))), + ); + $form['feed']['block'] = array( + '#type' => 'select', + '#title' => t('News items in block'), + '#default_value' => empty($form['#node']->feed->block) ? 5 : $form['#node']->feed->block, + '#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)), + '#description' => t("Drupal can make a block with the most recent news items of this feed. You can configure blocks to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in this feed's block. If you choose '0' this feed's block will be disabled.", array('@block-admin' => url('admin/build/block'))), + ); + } +} +/** * Add/edit/delete an aggregator item. * * @param $edit @@ -146,7 +191,7 @@ 'description' => $edit['description'], 'guid' => $edit['guid'], 'timestamp' => $edit['timestamp'], - 'fid' => $edit['fid'], + 'nid' => $edit['nid'], )) ->execute(); } @@ -160,7 +205,7 @@ } elseif ($edit['title'] && $edit['link']) { // file the items in the categories indicated by the feed - $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $edit['fid'])); + $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE vid = :vid', array(':vid' => $edit['vid'])); foreach ($result as $category) { db_merge('aggregator_category_item') ->key(array('iid' => $edit['iid'])) @@ -173,6 +218,28 @@ } /** + * Saves a feed node's category associations. + */ +function aggregator_processor_save_feed_category($feed) { + db_delete('aggregator_category_feed') + ->condition('vid', $feed['vid']) + ->execute(); + if (@is_array($feed['category'])) { + foreach ($feed['category'] as $key => $value) { + if (!empty($value)) { + db_insert('aggregator_category_feed') + ->fields(array( + 'nid' => $feed['nid'], + 'vid' => $feed['vid'], + 'cid' => $value, + )) + ->execute(); + } + } + } +} + +/** * Expire feed items on $feed that are older than aggregator_clear. * * @param $feed @@ -184,8 +251,8 @@ if ($aggregator_clear != AGGREGATOR_CLEAR_NEVER) { // Remove all items that are older than flush item timer. $age = REQUEST_TIME - $aggregator_clear; - $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid AND timestamp < :timestamp', array( - ':fid' => $feed->fid, + $iids = db_query('SELECT iid FROM {aggregator_item} WHERE nid = :nid AND timestamp < :timestamp', array( + ':nid' => $feed->nid, ':timestamp' => $age, )) ->fetchCol(); Index: modules/aggregator/aggregator.test =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.test,v retrieving revision 1.26 diff -u -r1.26 aggregator.test --- modules/aggregator/aggregator.test 12 Jun 2009 08:39:35 -0000 1.26 +++ modules/aggregator/aggregator.test 21 Jun 2009 15:44:29 -0000 @@ -9,7 +9,7 @@ class AggregatorTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('aggregator', 'aggregator_test'); - $web_user = $this->drupalCreateUser(array('administer news feeds', 'access news feeds', 'create article content')); + $web_user = $this->drupalCreateUser(array('administer news feeds', 'access news feeds', 'create article content', 'create feed content', 'edit any feed content', 'delete any feed content')); $this->drupalLogin($web_user); } @@ -18,17 +18,21 @@ * * @param $feed_url * If given, feed will be created with this URL, otherwise /rss.xml will be used. + * @param $content_type + * Machine readable content type. * @return $feed * Full feed object if possible. * * @see getFeedEditArray() */ - function createFeed($feed_url = NULL) { + function createFeed($content_type = 'feed', $feed_url = NULL) { $edit = $this->getFeedEditArray($feed_url); - $this->drupalPost('admin/content/aggregator/add/feed', $edit, t('Save')); - $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), t('The feed !name has been added.', array('!name' => $edit['title']))); + $this->drupalPost('node/add/' . $content_type, $edit, t('Save')); + $type_names = node_type_get_names(); + $test_string = t('!name %title has been created.', array('!name' => $type_names[$content_type], '%title' => $edit['title'])); + $this->assertRaw($test_string, $test_string); - $feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $edit['title'], ':url' => $edit['url']))->fetch(); + $feed = db_query("SELECT * FROM {aggregator_feed} f INNER JOIN {node} n ON n.nid = f.nid WHERE n.title = :title AND f.url = :url", array(':title' => $edit['title'], ':url' => $edit['feed[url]']))->fetch(); $this->assertTrue(!empty($feed), t('The feed found in database.')); return $feed; } @@ -40,8 +44,8 @@ * Feed object representing the feed. */ function deleteFeed($feed) { - $this->drupalPost('admin/content/aggregator/edit/feed/' . $feed->fid, array(), t('Delete')); - $this->assertRaw(t('The feed %title has been deleted.', array('%title' => $feed->title)), t('Feed deleted successfully.')); + $this->drupalPost('node/' . $feed->nid . '/delete', array(), t('Delete')); + $this->assertRaw(t('Feed %title has been deleted.', array('%title' => $feed->title)), t('Feed deleted successfully.')); } /** @@ -62,8 +66,8 @@ } $edit = array( 'title' => $feed_name, - 'url' => $feed_url, - 'refresh' => '900', + 'feed[url]' => $feed_url, + 'feed[refresh]' => '900', ); return $edit; } @@ -81,7 +85,7 @@ } /** - * Update feed items (simulate click to admin/content/aggregator/update/$fid). + * Update feed items (simulate click to node/$nid/update). * * @param $feed * Feed object representing the feed. @@ -94,10 +98,10 @@ $this->assertResponse(200, t('!url is reachable.', array('!url' => $feed->url))); // Refresh the feed (simulated link click). - $this->drupalGet('admin/content/aggregator/update/' . $feed->fid); + $this->drupalGet('node/' . $feed->nid . '/update'); // Ensure we have the right number of items. - $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid)); + $result = db_query('SELECT iid FROM {aggregator_item} WHERE nid = :nid', array(':nid' => $feed->nid)); $items = array(); $feed->items = array(); foreach ($result as $item) { @@ -114,7 +118,7 @@ * Feed object representing the feed. */ function removeFeedItems($feed) { - $this->drupalPost('admin/content/aggregator/remove/' . $feed->fid, array(), t('Remove items')); + $this->drupalPost('node/' . $feed->nid . '/remove-items', array(), t('Remove items')); $this->assertRaw(t('The news items from %title have been removed.', array('%title' => $feed->title)), t('Feed items removed.')); } @@ -130,7 +134,7 @@ $this->updateFeedItems($feed, $expected_count); $this->assertText('There is new syndicated content from'); $this->removeFeedItems($feed); - $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(); + $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE nid = :nid', array(':nid' => $feed->nid))->fetchField(); $this->assertTrue($count == 0); } @@ -142,7 +146,7 @@ */ function getFeedCategories($feed) { // add the categories to the feed so we can use them - $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $feed->fid)); + $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE nid = :nid', array(':nid' => $feed->nid)); foreach ($result as $category) { $feed->categories[] = $category->cid; } @@ -159,7 +163,7 @@ * TRUE if feed is unique. */ function uniqueFeed($feed_name, $feed_url) { - $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed_name, ':url' => $feed_url))->fetchField(); + $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} f INNER JOIN {node} n ON n.vid = f.vid WHERE n.title = :title AND f.url = :url", array(':title' => $feed_name, ':url' => $feed_url))->fetchField(); return (1 == $result); } @@ -174,7 +178,7 @@ function getValidOpml($feeds) { // Properly escape URLs so that XML parsers don't choke on them. foreach ($feeds as &$feed) { - $feed['url'] = htmlspecialchars($feed['url']); + $feed['feed[url]'] = htmlspecialchars($feed['feed[url]']); } /** * Does not have an XML declaration, must pass the parser. @@ -184,18 +188,18 @@ -