Index: modules/aggregator/aggregator.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.install,v
retrieving revision 1.16
diff -u -r1.16 aggregator.install
--- modules/aggregator/aggregator.install	15 May 2008 21:27:32 -0000	1.16
+++ modules/aggregator/aggregator.install	9 Aug 2008 21:16:58 -0000
@@ -64,11 +64,12 @@
   $schema['aggregator_category_feed'] = array(
     'description' => t('Bridge table; maps feeds to categories.'),
     'fields' => array(
-      'fid' => array(
+      'nid' => array(
         'type' => 'int',
+        'unsigned' => TRUE,
         'not null' => TRUE,
         'default' => 0,
-        'description' => t("The feed's {aggregator_feed}.fid."),
+        'description' => t("The feed's {aggregator_feed}.nid."),
       ),
       'cid' => array(
         'type' => 'int',
@@ -77,9 +78,9 @@
         'description' => t('The {aggregator_category}.cid to which the feed is being assigned.'),
       )
     ),
-    'primary key' => array('cid', 'fid'),
+    'primary key' => array('cid', 'nid'),
     'indexes' => array(
-      'fid' => array('fid'),
+      'nid' => array('nid'),
     ),
   );
 
@@ -108,17 +109,12 @@
   $schema['aggregator_feed'] = array(
     'description' => t('Stores feeds to be parsed by the aggregator.'),
     'fields' => array(
-      'fid' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => t('Primary Key: Unique feed ID.'),
-      ),
-      'title' => array(
-        'type' => 'varchar',
-        'length' => 255,
+      'nid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
         'not null' => TRUE,
-        'default' => '',
-        'description' => t('Title of the feed.'),
+        'default' => 0,
+        'description' => t("The feed's {node}.nid."),
       ),
       'url' => array(
         'type' => 'varchar',
@@ -179,10 +175,9 @@
         'description' => t("Number of items to display in the feed's block."),
       )
     ),
-    'primary key' => array('fid'),
+    'primary key' => array('nid'),
     'unique keys' => array(
       'url'  => array('url'),
-      'title' => array('title'),
     ),
   );
 
@@ -194,11 +189,12 @@
         'not null' => TRUE,
         'description' => t('Primary Key: Unique ID for feed item.'),
       ),
-      'fid' => array(
+      'nid' => array(
         'type' => 'int',
+        'unsigned' => TRUE,
         'not null' => TRUE,
         'default' => 0,
-        'description' => t('The {aggregator_feed}.fid to which this item belongs.'),
+        'description' => t('The {aggregator_feed}.nid to which this item belongs.'),
       ),
       'title' => array(
         'type' => 'varchar',
@@ -241,7 +237,7 @@
     ),
     'primary key' => array('iid'),
     'indexes' => array(
-      'fid' => array('fid'),
+      'nid' => array('nid'),
     ),
   );
 
Index: modules/aggregator/aggregator.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.pages.inc,v
retrieving revision 1.15
diff -u -r1.15 aggregator.pages.inc
--- modules/aggregator/aggregator.pages.inc	16 Jul 2008 21:59:25 -0000	1.15
+++ modules/aggregator/aggregator.pages.inc	9 Aug 2008 21:16:58 -0000
@@ -15,7 +15,7 @@
 function aggregator_page_last() {
   drupal_add_feed(url('aggregator/rss'), variable_get('site_name', 'Drupal') . ' ' . t('aggregator'));
 
-  $items = aggregator_feed_items_load('SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_item} i INNER JOIN {aggregator_feed} f ON i.fid = f.fid ORDER BY i.timestamp DESC, i.iid DESC');
+  $items = aggregator_feed_items_load('SELECT i.*, n.title AS ftitle, f.link AS flink FROM {aggregator_item} i INNER JOIN {aggregator_feed} f ON i.nid = f.nid LEFT JOIN {node} n ON f.nid = n.nid ORDER BY i.timestamp DESC, i.iid DESC');
 
   return _aggregator_page_list($items, arg(1));
 }
@@ -35,16 +35,16 @@
 function aggregator_page_source($arg1, $arg2 = NULL) {
   // If there are two arguments then this function is the categorize form, and
   // $arg1 is $form_state and $arg2 is $feed. Otherwise, $arg1 is $feed.
-  $feed = is_array($arg2) ? $arg2 : $arg1;
+  $feed = is_object($arg2) ? $arg2 : $arg1;
   $feed = (object)$feed;
   drupal_set_title(check_plain($feed->title));
   $feed_source = theme('aggregator_feed_source', $feed);
 
   // It is safe to include the fid in the query because it's loaded from the
   // database by aggregator_feed_load.
-  $items = aggregator_feed_items_load('SELECT * FROM {aggregator_item} WHERE fid = ' . $feed->fid . ' ORDER BY timestamp DESC, iid DESC');
+  $items = aggregator_feed_items_load('SELECT * FROM {aggregator_item} WHERE nid = ' . $feed->nid . ' ORDER BY timestamp DESC, iid DESC');
 
-  return _aggregator_page_list($items, arg(3), $feed_source);
+  return _aggregator_page_list($items, arg(2), $feed_source);
 }
 
 /**
@@ -69,7 +69,7 @@
 
   // It is safe to include the cid in the query because it's loaded from the
   // database by aggregator_category_load.
-  $items = aggregator_feed_items_load('SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = ' . $category['cid'] . ' ORDER BY timestamp DESC, i.iid DESC');
+  $items = aggregator_feed_items_load('SELECT i.*, n.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.nid = f.nid LEFT JOIN {node} n ON f.nid = n.nid WHERE cid = ' . $category['cid'] . ' ORDER BY timestamp DESC, i.iid DESC');
 
   return _aggregator_page_list($items, arg(3));
 }
@@ -282,19 +282,19 @@
  * Menu callback; displays all the feeds used by the aggregator.
  */
 function aggregator_page_sources() {
-  $result = db_query('SELECT f.fid, f.title, f.description, f.image, MAX(i.timestamp) AS last FROM {aggregator_feed} f LEFT JOIN {aggregator_item} i ON f.fid = i.fid GROUP BY f.fid, f.title, f.description, f.image ORDER BY last DESC, f.title');
+  $result = db_query('SELECT f.nid, n.title, f.description, f.image, MAX(i.timestamp) AS last FROM {aggregator_feed} f LEFT JOIN {node} n ON n.nid = f.nid LEFT JOIN {aggregator_item} i ON f.nid = i.nid GROUP BY f.nid, n.title, f.description, f.image ORDER BY last DESC, n.title');
 
   $output = '';
   while ($feed = db_fetch_object($result)) {
     // Most recent items:
     $summary_items = array();
     if (variable_get('aggregator_summary_items', 3)) {
-      $items = db_query_range('SELECT i.title, i.timestamp, i.link FROM {aggregator_item} i WHERE i.fid = %d ORDER BY i.timestamp DESC', $feed->fid, 0, variable_get('aggregator_summary_items', 3));
+      $items = db_query_range('SELECT i.title, i.timestamp, i.link FROM {aggregator_item} i WHERE i.nid = %d ORDER BY i.timestamp DESC', $feed->nid, 0, variable_get('aggregator_summary_items', 3));
       while ($item = db_fetch_object($items)) {
         $summary_items[] = theme('aggregator_summary_item', $item);
       }
     }
-    $feed->url = url('aggregator/sources/' . $feed->fid);
+    $feed->url = url('node/' . $feed->nid);
     $output .= theme('aggregator_summary_items', $summary_items, $feed);
   }
   $output .= theme('feed_icon', url('aggregator/opml'), t('OPML feed'));
@@ -312,7 +312,7 @@
   while ($category = db_fetch_object($result)) {
     if (variable_get('aggregator_summary_items', 3)) {
       $summary_items = array();
-      $items = db_query_range('SELECT i.title, i.timestamp, i.link, f.title as feed_title, f.link as feed_link FROM {aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON i.iid = ci.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE ci.cid = %d ORDER BY i.timestamp DESC', $category->cid, 0, variable_get('aggregator_summary_items', 3));
+      $items = db_query_range('SELECT i.title, i.timestamp, i.link, n.title AS feed_title, f.link AS feed_link FROM {aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON i.iid = ci.iid LEFT JOIN {aggregator_feed} f ON i.nid = f.nid LEFT JOIN {node} n ON f.nid = n.nid WHERE ci.cid = %d ORDER BY i.timestamp DESC', $category->cid, 0, variable_get('aggregator_summary_items', 3));
       while ($item = db_fetch_object($items)) {
         $summary_items[] = theme('aggregator_summary_item', $item);
       }
@@ -338,7 +338,7 @@
   // Or, get the default aggregator items.
   else {
     $category = NULL;
-    $sql = 'SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_item} i INNER JOIN {aggregator_feed} f ON i.fid = f.fid ORDER BY i.timestamp DESC, i.iid DESC';
+    $sql = 'SELECT i.*, n.title AS ftitle, f.link AS flink FROM {aggregator_item} i INNER JOIN {aggregator_feed} f ON i.nid = f.nid LEFT JOIN {node} n ON n.nid = f.nid ORDER BY i.timestamp DESC, i.iid DESC';
     $result = db_query_range($sql, 0, variable_get('feed_default_items', 10));
   }
 
@@ -402,10 +402,10 @@
  */
 function aggregator_page_opml($cid = NULL) {
   if ($cid) {
-    $result = db_query('SELECT f.title, f.url FROM {aggregator_feed} f LEFT JOIN {aggregator_category_feed} c on f.fid = c.fid WHERE c.cid = %d ORDER BY title', $cid);
+    $result = db_query('SELECT n.title, f.url FROM {node} n LEFT JOIN {aggregator_feed} f ON f.nid = n.nid LEFT JOIN {aggregator_category_feed} c on f.nid = c.nid WHERE c.cid = %d ORDER BY title', $cid);
   }
   else {
-    $result = db_query('SELECT * FROM {aggregator_feed} ORDER BY title');
+    $result = db_query('SELECT f.*, n.title FROM {aggregator_feed} f LEFT JOIN {node} n ON n.nid = f.nid ORDER BY n.title');
   }
 
   $feeds = array();
Index: modules/aggregator/aggregator.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.module,v
retrieving revision 1.385
diff -u -r1.385 aggregator.module
--- modules/aggregator/aggregator.module	8 Aug 2008 20:09:22 -0000	1.385
+++ modules/aggregator/aggregator.module	9 Aug 2008 21:16:58 -0000
@@ -87,13 +87,10 @@
     '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/list'] = array(
+    'title' => 'List',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
   );
   $items['admin/content/aggregator/add/category'] = array(
     'title' => 'Add category',
@@ -125,11 +122,6 @@
     '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/content/aggregator/settings'] = array(
     'title' => 'Settings',
     'page callback' => 'drupal_get_form',
@@ -194,39 +186,6 @@
     'type' => MENU_LOCAL_TASK,
     'weight' => 1,
   );
-  $items['aggregator/sources/%aggregator_feed'] = 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(
-    'title' => 'View',
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-    'weight' => -10,
-  );
-  $items['aggregator/sources/%aggregator_feed/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',
@@ -235,6 +194,14 @@
     'type' => MENU_CALLBACK,
   );
 
+  $items['node/%node/categorize'] = array(
+    'title' => 'Categorize',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('aggregator_page_source', 1),
+    'access arguments' => array('administer news feeds'),
+    'type' => MENU_LOCAL_TASK,
+  );
+
   return $items;
 }
 
@@ -269,10 +236,13 @@
  * Implementation of hook_perm().
  */
 function aggregator_perm() {
-  return array(
+  $perms = node_list_permissions('feed');
+  $perms += array(
     'administer news feeds' => t('Add, edit or delete news feeds that are aggregated to your site.'),
     'access news feeds' => t('View aggregated news feed items.'),
   );
+
+  return $perms;
 }
 
 /**
@@ -299,9 +269,9 @@
       while ($category = db_fetch_object($result)) {
         $block['category-' . $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title));
       }
-      $result = db_query('SELECT fid, title FROM {aggregator_feed} ORDER BY fid');
+      $result = db_query('SELECT f.nid, n.title FROM {aggregator_feed} f LEFT JOIN {node} n ON n.nid = f.nid ORDER BY f.nid');
       while ($feed = db_fetch_object($result)) {
-        $block['feed-' . $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title));
+        $block['feed-' . $feed->nid]['info'] = t('!title feed latest items', array('!title' => $feed->title));
       }
     }
     elseif ($op == 'configure') {
@@ -310,7 +280,7 @@
         $value = db_result(db_query('SELECT block FROM {aggregator_category} WHERE cid = %d', $id));
       }
       else {
-        $value = db_result(db_query('SELECT block FROM {aggregator_feed} WHERE fid = %d', $id));
+        $value = db_result(db_query('SELECT block FROM {aggregator_feed} WHERE nid = %d', $id));
       }
       $form['block'] = array('#type' => 'select', '#title' => t('Number of news items in block'), '#default_value' => $value, '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)));
       return $form;
@@ -321,17 +291,17 @@
         $value = db_query('UPDATE {aggregator_category} SET block = %d WHERE cid = %d', $edit['block'], $id);
       }
       else {
-        $value = db_query('UPDATE {aggregator_feed} SET block = %d WHERE fid = %d', $edit['block'], $id);
+        $value = db_query('UPDATE {aggregator_feed} SET block = %d WHERE nid = %d', $edit['block'], $id);
       }
     }
     elseif ($op == 'view') {
       list($type, $id) = explode('-', $delta);
       switch ($type) {
         case 'feed':
-          if ($feed = db_fetch_object(db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE fid = %d', $id))) {
+          if ($feed = db_fetch_object(db_query('SELECT f.nid, n.title, f.block FROM {aggregator_feed} f LEFT JOIN {node} n ON n.nid = f.nid WHERE f.nid = %d', $id))) {
             $block['subject'] = check_plain($feed->title);
-            $result = db_query_range('SELECT * FROM {aggregator_item} WHERE fid = %d ORDER BY timestamp DESC, iid DESC', $feed->fid, 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 = %d ORDER BY timestamp DESC, iid DESC', $feed->nid, 0, $feed->block);
+            $read_more = theme('more_link', url('node/' . $feed->nid), t("View this feed's recent news."));
           }
           break;
 
@@ -360,6 +330,236 @@
 }
 
 /**
+ * Implementation of hook_node_info().
+ */
+function aggregator_node_info() {
+  return array(
+    'feed' => array(
+      'name' => t('Feed'),
+      'module' => 'aggregator',
+      'description' => t("Add a feed in RSS, RDF or Atom format."),
+      'has_title' => TRUE,
+      'title_label' => t('Title'),
+      'has_body' => FALSE,
+    )
+  );
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function aggregator_access($op, $node, $account) {
+  switch ($op) {
+    case 'create':
+      return user_access('create feed content', $account);
+    case 'update':
+      return user_access('edit any feed content', $account) || (user_access('edit own feed content', $account) && ($node->uid == $account->uid));
+    case 'delete':
+      return user_access('delete any feed content', $account) || (user_access('delete own feed content', $account) && ($node->uid == $account->uid));
+  }
+}
+
+/**
+ * Implementation of hook_form().
+ */
+function aggregator_form(&$node) {
+  global $user;
+
+  $admin = user_access('administer nodes') || user_access('edit any feed content') || (user_access('edit own feed content') && $user->uid == $node->uid);
+
+  // The site admin can decide if this node type has a title and body, and how
+  // the fields should be labeled. We need to load these settings so we can
+  // build the node form correctly.
+  $type = node_get_types('type', $node);
+
+  if ($type->has_title) {
+    $form['title'] = array(
+      '#type' => 'textfield',
+      '#title' => check_plain($type->title_label),
+      '#description' => t('The name of the feed (or the name of the website providing the feed).'),
+      '#default_value' => $node->title,
+      '#maxlength' => 255,
+      '#required' => TRUE,
+      '#weight' => -5
+    );
+  }
+
+  if ($type->has_body) {
+    // In Drupal 6, we can use node_body_field() to get the body and filter
+    // elements. This replaces the old textarea + filter_form() method of
+    // setting this up. It will also ensure the teaser splitter gets set up
+    // properly.
+    $form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
+  }
+
+  $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
+
+  $form['url'] = array(
+    '#type' => 'textfield',
+    '#title' => t('URL'),
+    '#default_value' => isset($node->url) ? $node->url : '',
+    '#maxlength' => 255,
+    '#description' => t('The fully-qualified URL of the feed.'),
+    '#required' => TRUE,
+    '#weight' => -4
+  );
+
+  $form['settings'] = array(
+    '#type' => 'fieldset',
+    '#collapsible' => TRUE,
+    '#title' => t('Feed settings'),
+    '#weight' => -3,
+    '#access' => $admin,
+  );
+  $form['settings']['refresh'] = array(
+    '#type' => 'select',
+    '#title' => t('Update interval'),
+    '#default_value' => isset($node->refresh) ? $node->refresh : 3600,
+    '#options' => $period,
+    '#description' => t('The length of time between feed updates. (Requires a correctly configured <a href="@cron">cron maintenance task</a>.)', array('@cron' => url('admin/reports/status'))),
+  );
+
+  // Handling of categories.
+  $options = array();
+  $values = isset($node->categories) ? $node->categories : array();
+  $categories = db_query('SELECT cid, title FROM {aggregator_category} ORDER BY title');
+  while ($category = db_fetch_object($categories)) {
+    $options[$category->cid] = check_plain($category->title);
+    if (isset($node->categories) && in_array($category->cid, $node->categories)) {
+      $values[] = $category->cid;
+    }
+  }
+  if ($options) {
+    $form['settings']['categories'] = 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.'),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Implementation of hook_validate().
+ */
+function aggregator_validate(&$node) {
+  // Ensure URL is valid.
+  if (!valid_url($node->url, TRUE)) {
+    form_set_error('url', t('The URL %url is invalid. Please enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $node->url)));
+  }
+  // Check for duplicate titles.
+  if (isset($node->nid)) {
+    $result_title = db_result(db_query("SELECT title FROM {node} WHERE title = '%s' AND nid <> %d AND type = 'feed'", $node->title, $node->nid));
+    $result_url = db_result(db_query("SELECT url FROM {aggregator_feed} WHERE url = '%s' AND nid <> %d", $node->url, $node->nid));
+  }
+  else {
+    $result_title = db_result(db_query("SELECT title FROM {node} WHERE title = '%s' AND type = 'feed'", $node->title));
+    $result_url = db_result(db_query("SELECT url FROM {aggregator_feed} WHERE url = '%s'", $node->url));
+  }
+
+  if (strcasecmp($result_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($result_url, $node->url) == 0) {
+    form_set_error('url', t('A feed with this URL %url already exists. Please enter a unique URL.', array('%url' => $node->url)));
+  }
+}
+
+/**
+ * Implementation of hook_insert().
+ */
+function aggregator_insert($node) {
+  db_query("INSERT INTO {aggregator_feed} (nid, url, refresh, block, description, image) VALUES (%d, '%s', %d, 5, '', '')", $node->nid, $node->url, $node->refresh);
+
+  // The feed is being saved, save the categories as well.
+  if (!empty($node->categories)) {
+    foreach ($node->categories as $cid => $value) {
+      if ($value) {
+        db_query('INSERT INTO {aggregator_category_feed} (nid, cid) VALUES (%d, %d)', $node->nid, $cid);
+      }
+    }
+  }
+}
+
+/**
+ * Implementation of hook_update().
+ */
+function aggregator_update($node) {
+  // An existing feed is being modified, delete the category listings.
+  db_query('DELETE FROM {aggregator_category_feed} WHERE nid = %d', $node->nid);
+
+  db_query("UPDATE {aggregator_feed} SET url = '%s', refresh = %d WHERE nid = %d", $node->url, $node->refresh, $node->nid);
+
+  // The feed is being saved, save the categories as well.
+  if (!empty($node->categories)) {
+    foreach ($node->categories as $cid => $value) {
+      if ($value) {
+        db_query('INSERT INTO {aggregator_category_feed} (nid, cid) VALUES (%d, %d)', $node->nid, $cid);
+      }
+    }
+  }
+}
+
+/**
+ * Implementation of hook_delete().
+ */
+function aggregator_delete($node) {
+  // An existing feed is being modified, delete the category listings.
+  db_query('DELETE FROM {aggregator_category_feed} WHERE nid = %d', $node->nid);
+
+  $items = array();
+  $result = db_query('SELECT iid FROM {aggregator_item} WHERE nid = %d', $node->nid);
+  while ($item = db_fetch_object($result)) {
+    $items[] = "iid = $item->iid";
+  }
+  if (!empty($items)) {
+    db_query('DELETE FROM {aggregator_category_item} WHERE ' . implode(' OR ', $items));
+  }
+  db_query('DELETE FROM {aggregator_item} WHERE nid = %d', $node->nid);
+  db_query('DELETE FROM {aggregator_feed} WHERE nid = %d', $node->nid);
+  // Make sure there is no active block for this feed.
+  db_query("DELETE FROM {blocks} WHERE module = '%s' AND delta = '%s'", 'aggregator', 'feed-' . $node->nid);
+}
+
+/**
+ * Implementation of hook_load().
+ */
+function aggregator_load($node) {
+  $additions = db_fetch_object(db_query('SELECT url, refresh, checked, link, description, image, etag, modified, block FROM {aggregator_feed} WHERE nid = %d', $node->nid));
+  $categories = db_query('SELECT cid FROM {aggregator_category_feed} WHERE nid = %d', $node->nid);
+  while ($category = db_fetch_object($categories)) {
+    $additions->categories[] = $category->cid;
+  }
+  return $additions;
+}
+
+/**
+ * Implementation of hook_view().
+ */
+function aggregator_view($node, $teaser = FALSE, $page = FALSE) {
+  $node->content['body'] = array(
+    '#markup' => theme('aggregator_feed_source', $node),
+    '#weight' => 1,
+  );
+
+  if ($page && isset($node->nid)) {
+    // It is safe to include the fid in the query because it's loaded from the
+    // database by aggregator_feed_load.
+    $items = aggregator_feed_items_load('SELECT * FROM {aggregator_item} WHERE nid = ' . $node->nid . ' ORDER BY timestamp DESC, iid DESC');
+  
+    $node->content['items'] = array(
+      '#markup' => _aggregator_page_list($items, arg(3)),
+      '#weight' => 2,
+    );
+  }
+
+  return $node;
+}
+
+/**
  * Add/edit/delete aggregator categories.
  *
  * @param $edit
@@ -444,15 +644,15 @@
  *   An associative array describing the feed to be cleared.
  */
 function aggregator_remove($feed) {
-  $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
+  $result = db_query('SELECT iid FROM {aggregator_item} WHERE nid = %d', $feed['nid']);
   while ($item = db_fetch_object($result)) {
     $items[] = "iid = $item->iid";
   }
   if (!empty($items)) {
     db_query('DELETE FROM {aggregator_category_item} WHERE ' . implode(' OR ', $items));
   }
-  db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
-  db_query("UPDATE {aggregator_feed} SET checked = 0, etag = '', modified = 0 WHERE fid = %d", $feed['fid']);
+  db_query('DELETE FROM {aggregator_item} WHERE nid = %d', $feed['nid']);
+  db_query("UPDATE {aggregator_feed} SET checked = 0, etag = '', modified = 0 WHERE nid = %d", $feed['nid']);
   drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed['title'])));
 }
 
@@ -578,6 +778,8 @@
 function aggregator_refresh($feed) {
   global $channel, $image;
 
+  $feed['title'] = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $feed['nid']));
+
   // Generate conditional GET headers.
   $headers = array();
   if ($feed['etag']) {
@@ -593,7 +795,7 @@
   // Process HTTP response code.
   switch ($result->code) {
     case 304:
-      db_query('UPDATE {aggregator_feed} SET checked = %d WHERE fid = %d', time(), $feed['fid']);
+      db_query('UPDATE {aggregator_feed} SET checked = %d WHERE fid = %d', time(), $feed['nid']);
       drupal_set_message(t('There is no new syndicated content from %site.', array('%site' => $feed['title'])));
       break;
     case 301:
@@ -628,7 +830,7 @@
 
         $etag = empty($result->headers['ETag']) ? '' : $result->headers['ETag'];
         // Update the feed data.
-        db_query("UPDATE {aggregator_feed} SET url = '%s', checked = %d, link = '%s', description = '%s', image = '%s', etag = '%s', modified = %d WHERE fid = %d", $feed['url'], time(), $channel['LINK'], $channel['DESCRIPTION'], $image, $etag, $modified, $feed['fid']);
+        db_query("UPDATE {aggregator_feed} SET url = '%s', checked = %d, link = '%s', description = '%s', image = '%s', etag = '%s', modified = %d WHERE nid = %d", $feed['url'], time(), $channel['LINK'], $channel['DESCRIPTION'], $image, $etag, $modified, $feed['nid']);
 
         // Clear the cache.
         cache_clear_all();
@@ -705,6 +907,8 @@
   $image = array();
   $channel = array();
 
+  $feed = (array) $feed;
+
   // Parse the data.
   $xml_parser = drupal_xml_parser_create($data);
   xml_set_element_handler($xml_parser, 'aggregator_element_start', 'aggregator_element_end');
@@ -788,21 +992,21 @@
     // we find a duplicate entry, we resolve it and pass along its ID is such
     // that we can update it if needed.
     if (!empty($guid)) {
-      $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND guid = '%s'", $feed['fid'], $guid));
+      $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE nid = %d AND guid = '%s'", $feed['nid'], $guid));
     }
     else if ($link && $link != $feed['link'] && $link != $feed['url']) {
-      $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND link = '%s'", $feed['fid'], $link));
+      $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE nid = %d AND link = '%s'", $feed['nid'], $link));
     }
     else {
-      $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND title = '%s'", $feed['fid'], $title));
+      $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE nid = %d AND title = '%s'", $feed['nid'], $title));
     }
     $item += array('AUTHOR' => '', 'DESCRIPTION' => '');
-    aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid:  ''), 'fid' => $feed['fid'], 'timestamp' => $timestamp, 'title' => $title, 'link' => $link, 'author' => $item['AUTHOR'], 'description' => $item['DESCRIPTION'], 'guid' => $guid));
+    aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid:  ''), 'nid' => $feed['nid'], 'timestamp' => $timestamp, 'title' => $title, 'link' => $link, 'author' => $item['AUTHOR'], 'description' => $item['DESCRIPTION'], 'guid' => $guid));
   }
 
   // Remove all items that are older than flush item timer.
   $age = time() - variable_get('aggregator_clear', 9676800);
-  $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
+  $result = db_query('SELECT iid FROM {aggregator_item} WHERE nid = %d AND timestamp < %d', $feed['nid'], $age);
 
   $items = array();
   $num_rows = FALSE;
@@ -812,7 +1016,7 @@
   }
   if ($num_rows) {
     db_query('DELETE FROM {aggregator_category_item} WHERE iid IN (' . implode(', ', $items) . ')');
-    db_query('DELETE FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
+    db_query('DELETE FROM {aggregator_item} WHERE nid = %d AND timestamp < %d', $feed['nid'], $age);
   }
 
   return TRUE;
@@ -833,10 +1037,10 @@
     db_query('DELETE FROM {aggregator_category_item} WHERE iid = %d', $edit['iid']);
   }
   elseif ($edit['title'] && $edit['link']) {
-    db_query("INSERT INTO {aggregator_item} (fid, title, link, author, description, timestamp, guid) VALUES (%d, '%s', '%s', '%s', '%s', %d, '%s')", $edit['fid'], $edit['title'], $edit['link'], $edit['author'], $edit['description'], $edit['timestamp'], $edit['guid']);
+    db_query("INSERT INTO {aggregator_item} (nid, title, link, author, description, timestamp, guid) VALUES (%d, '%s', '%s', '%s', '%s', %d, '%s')", $edit['nid'], $edit['title'], $edit['link'], $edit['author'], $edit['description'], $edit['timestamp'], $edit['guid']);
     $edit['iid'] = db_last_insert_id('aggregator_item', 'iid');
     // file the items in the categories indicated by the feed
-    $categories = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
+    $categories = db_query('SELECT cid FROM {aggregator_category_feed} WHERE nid = %d', $edit['nid']);
     while ($category = db_fetch_object($categories)) {
       db_query('INSERT INTO {aggregator_category_item} (cid, iid) VALUES (%d, %d)', $category->cid, $edit['iid']);
     }
@@ -846,18 +1050,18 @@
 /**
  * Load an aggregator feed.
  *
- * @param $fid
- *   The feed id.
+ * @param $nid
+ *   The feed nid.
  * @return
  *   An associative array describing the feed.
  */
-function aggregator_feed_load($fid) {
+function aggregator_feed_load($nid) {
   static $feeds;
-  if (!isset($feeds[$fid])) {
-    $feeds[$fid] = db_fetch_array(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', $fid));
+  if (!isset($feeds[$nid])) {
+    $feeds[$nid] = db_fetch_array(db_query('SELECT * FROM {aggregator_feed} WHERE nid = %d', $nid));
   }
 
-  return $feeds[$fid];
+  return $feeds[$nid];
 }
 
 /**
Index: modules/aggregator/aggregator.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.admin.inc,v
retrieving revision 1.11
diff -u -r1.11 aggregator.admin.inc
--- modules/aggregator/aggregator.admin.inc	3 Aug 2008 05:46:55 -0000	1.11
+++ modules/aggregator/aggregator.admin.inc	9 Aug 2008 21:16:57 -0000
@@ -8,26 +8,23 @@
 
 /**
  * Menu callback; displays the aggregator administration page.
- */
-function aggregator_admin_overview() {
-  return aggregator_view();
-}
-
-/**
- * Displays the aggregator administration page.
  *
  * @return
  *   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.etag, f.modified, f.image, f.block ORDER BY f.title');
+function aggregator_admin_overview() {
+  $result = db_query('
+    SELECT f.*, n.title, COUNT(i.iid) AS items FROM {aggregator_feed} f LEFT JOIN ({aggregator_item} i CROSS JOIN {node} n) ON (f.nid = i.nid AND f.nid = n.nid)
+      GROUP BY f.nid, n.title, f.url, f.refresh, f.checked, f.link, f.description, f.etag, f.modified, f.image, f.block
+      ORDER BY n.title'
+  );
 
   $output = '<h3>' . t('Feed overview') . '</h3>';
 
   $header = array(t('Title'), t('Items'), t('Last update'), t('Next update'), array('data' => t('Operations'), 'colspan' => '3'));
   $rows = array();
   while ($feed = db_fetch_object($result)) {
-    $rows[] = array(l($feed->title, "aggregator/sources/$feed->fid"), format_plural($feed->items, '1 item', '@count items'), ($feed->checked ? t('@time ago', array('@time' => format_interval(time() - $feed->checked))) : t('never')), ($feed->checked ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - time()))) : t('never')), l(t('edit'), "admin/content/aggregator/edit/feed/$feed->fid"), l(t('remove items'), "admin/content/aggregator/remove/$feed->fid"), l(t('update items'), "admin/content/aggregator/update/$feed->fid"));
+    $rows[] = array(l($feed->title, "node/$feed->nid"), format_plural($feed->items, '1 item', '@count items'), ($feed->checked ? t('@time ago', array('@time' => format_interval(time() - $feed->checked))) : t('never')), ($feed->checked ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - time()))) : t('never')), l(t('edit'), "node/$feed->nid/edit"), l(t('remove items'), "admin/content/aggregator/remove/$feed->nid"), l(t('update items'), "admin/content/aggregator/update/$feed->nid"));
   }
   $output .= theme('table', $header, $rows);
 
@@ -45,148 +42,9 @@
   return $output;
 }
 
-/**
- * Form builder; Generate a form to add/edit feed sources.
- *
- * @ingroup forms
- * @see aggregator_form_feed_validate()
- * @see aggregator_form_feed_submit()
- */
-function aggregator_form_feed(&$form_state, $edit = array('refresh' => 900, 'title' => '', 'url' => '', 'fid' => NULL)) {
-  $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
-
-  if ($edit['refresh'] == '') {
-    $edit['refresh'] = 3600;
-  }
-
-  $form['title'] = array('#type' => 'textfield',
-    '#title' => t('Title'),
-    '#default_value' => $edit['title'],
-    '#maxlength' => 255,
-    '#description' => t('The name of the feed (or the name of the website providing the feed).'),
-    '#required' => TRUE,
-  );
-  $form['url'] = array('#type' => 'textfield',
-    '#title' => t('URL'),
-    '#default_value' => $edit['url'],
-    '#maxlength' => 255,
-    '#description' => t('The fully-qualified URL of the feed.'),
-    '#required' => TRUE,
-  );
-  $form['refresh'] = array('#type' => 'select',
-    '#title' => t('Update interval'),
-    '#default_value' => $edit['refresh'],
-    '#options' => $period,
-    '#description' => t('The length of time between feed updates. (Requires a correctly configured <a href="@cron">cron maintenance task</a>.)', array('@cron' => url('admin/reports/status'))),
-  );
-
-  // Handling of categories.
-  $options = array();
-  $values = array();
-  $categories = db_query('SELECT c.cid, c.title, f.fid FROM {aggregator_category} c LEFT JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = %d ORDER BY title', $edit['fid']);
-  while ($category = db_fetch_object($categories)) {
-    $options[$category->cid] = check_plain($category->title);
-    if ($category->fid) $values[] = $category->cid;
-  }
-  if ($options) {
-    $form['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.'),
-    );
-  }
-  $form['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-  );
-
-  if ($edit['fid']) {
-    $form['delete'] = array(
-      '#type' => 'submit',
-      '#value' => t('Delete'),
-    );
-    $form['fid'] = array(
-      '#type' => 'hidden',
-      '#value' => $edit['fid'],
-    );
-  }
-
-  return $form;
-}
-
-/**
- * Validate aggregator_form_feed() form submissions.
- */
-function aggregator_form_feed_validate($form, &$form_state) {
-  if ($form_state['values']['op'] == t('Save')) {
-    // Ensure URL is valid.
-    if (!valid_url($form_state['values']['url'], TRUE)) {
-      form_set_error('url', t('The URL %url is invalid. Please enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $form_state['values']['url'])));
-    }
-    // Check for duplicate titles.
-    if (isset($form_state['values']['fid'])) {
-      $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE (title = '%s' OR url = '%s') AND fid <> %d", $form_state['values']['title'], $form_state['values']['url'], $form_state['values']['fid']);
-    }
-    else {
-      $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = '%s' OR url = '%s'", $form_state['values']['title'], $form_state['values']['url']);
-    }
-    while ($feed = db_fetch_object($result)) {
-      if (strcasecmp($feed->title, $form_state['values']['title']) == 0) {
-        form_set_error('title', t('A feed named %feed already exists. Please enter a unique title.', array('%feed' => $form_state['values']['title'])));
-      }
-      if (strcasecmp($feed->url, $form_state['values']['url']) == 0) {
-        form_set_error('url', t('A feed with this URL %url already exists. Please enter a unique URL.', array('%url' => $form_state['values']['url'])));
-      }
-    }
-  }
-}
-
-/**
- * Process aggregator_form_feed() form submissions.
- *
- * @todo Add delete confirmation dialog.
- */
-function aggregator_form_feed_submit($form, &$form_state) {
-  if ($form_state['values']['op'] == t('Delete')) {
-    $title = $form_state['values']['title'];
-    // Unset the title.
-    unset($form_state['values']['title']);
-  }
-  aggregator_save_feed($form_state['values']);
-  if (isset($form_state['values']['fid'])) {
-    if (isset($form_state['values']['title'])) {
-      drupal_set_message(t('The feed %feed has been updated.', array('%feed' => $form_state['values']['title'])));
-      if (arg(0) == 'admin') {
-        $form_state['redirect'] = 'admin/content/aggregator/';
-        return;
-      }
-      else {
-        $form_state['redirect'] = 'aggregator/sources/' . $form_state['values']['fid'];
-        return;
-      }
-    }
-    else {
-      watchdog('aggregator', 'Feed %feed deleted.', array('%feed' => $title));
-      drupal_set_message(t('The feed %feed has been deleted.', array('%feed' => $title)));
-      if (arg(0) == 'admin') {
-        $form_state['redirect'] = 'admin/content/aggregator/';
-        return;
-      }
-      else {
-        $form_state['redirect'] = 'aggregator/sources/';
-        return;
-      }
-    }
-  }
-  else {
-    watchdog('aggregator', 'Feed %feed added.', array('%feed' => $form_state['values']['title']), WATCHDOG_NOTICE, l(t('view'), 'admin/content/aggregator'));
-    drupal_set_message(t('The feed %feed has been added.', array('%feed' => $form_state['values']['title'])));
-  }
-}
-
 function aggregator_admin_remove_feed($form_state, $feed) {
+  $feed['title'] = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $feed['nid']));
+
   return confirm_form(
     array(
       'feed' => array(
