Index: modules/aggregator/aggregator.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.admin.inc,v
retrieving revision 1.7
diff -u -p -r1.7 aggregator.admin.inc
--- modules/aggregator/aggregator.admin.inc	10 Jan 2008 22:47:17 -0000	1.7
+++ modules/aggregator/aggregator.admin.inc	8 Feb 2008 11:31:42 -0000
@@ -204,6 +204,134 @@ function aggregator_admin_remove_feed_su
 }
 
 /**
+ * Form builder; Generate a form to import feeds from OPML.
+ *
+ * @ingroup forms
+ * @see aggregator_form_opml_submit()
+ */
+function aggregator_form_opml(&$form_state) {
+  $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
+
+  $form['#attributes'] = array('enctype' => "multipart/form-data");
+  
+  $form['upload'] = array('#type' => 'file',
+    '#title' => t('OPML File'),
+    '#description' => t('Upload an OPML file containing a list of newsfeeds to be imported.'),
+  );
+  $form['remote'] = array('#type' => 'textfield',
+    '#title' => t('OPML Remote URL'),
+    '#description' => t('Enter the URL of an OPML file on the web. This file will be downloaded only once.'),
+  );
+  
+  $form['refresh'] = array('#type' => 'select',
+    '#title' => t('Update interval'),
+    '#default_value' => 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 = 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 ($options) {
+    $form['category'] = array('#type' => 'checkboxes',
+      '#title' => t('Categorize news items'),
+      '#options' => $options,
+      '#description' => t('New feed items are automatically filed in the checked categories.'),
+    );
+  }
+  $form['submit'] = array('#type' => 'submit', '#value' => t('Import'));
+
+  return $form;
+}
+
+/**
+ * Validate aggregator_form_opml form submissions.
+ */
+function aggregator_form_opml_validate($form, &$form_state) {
+  if (!empty($form_state['values']['remote']) && !valid_url($form_state['values']['remote'], TRUE)) {
+    form_set_error('remote', t('This URL is not valid.'));
+  }
+}
+
+/**
+ * Process aggregator_form_opml form submissions.
+ */
+function aggregator_form_opml_submit($form, &$form_state) {
+  if ($file = file_save_upload('upload')) {
+    $data = file_get_contents($file->filepath);
+  } else {
+    $response = drupal_http_request($form_state['values']['remote']);
+    $data = $response->data;
+  }
+  $blogs = _aggregator_parse_opml($data);
+  if (!empty($blogs)) {
+    drupal_set_message(t('This OPML outline contains %num feeds.', array('%num' => count($blogs))), 'status');
+  } else {
+    drupal_set_message(t('The OPML outline could not be parsed.'), 'error');
+  }
+  
+  $form_state['values']['op'] = t('Save');
+
+  foreach($blogs as $blog) {
+    $duplicate = FALSE;
+    $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = '%s' OR url='%s'", $blog['title'], $blog['url']);
+    while (!$duplicate && $feed = db_fetch_object($result)) {
+      if (strcasecmp($feed->title, $form_state['values']['title']) == 0) {
+        drupal_set_message(t('A feed named %feed already exists and was not added.', array('%feed' => $form_state['values']['title'])), 'warning');
+        $duplicate = TRUE;
+      }
+      if (strcasecmp($feed->url, $form_state['values']['url']) == 0) {
+        drupal_set_message(t('A feed with the URL %url already exists and was not added.', array('%url' => $form_state['values']['url'])), 'warning');
+        $duplicate = TRUE;
+      }
+    }
+    if ($duplicate) continue;
+
+    $form_state['values']['title'] = $blog['title'];
+    $form_state['values']['url'] = $blog['url'];
+
+    drupal_execute('aggregator_form_feed', $form_state);
+  }
+}
+
+/**
+ * Parses an OPML file
+ */
+function _aggregator_parse_opml($opml) {
+
+  $feeds = array();
+  $error = FALSE;
+
+  $parser = drupal_xml_parser_create($opml);
+
+  //Some OPML Files don't have the xml tag, which causes parsing to fail. Hence, using the appended version as a fallback parse
+  if (xml_parse_into_struct($parser, $opml, $vals, $index) || xml_parse_into_struct($parser, '<?xml version="1.0"?>'. $opml, $vals, $index)) {
+
+    foreach($vals as $entry) {
+      if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) {
+        $feeds[] = $entry['attributes'];
+      }
+    }
+
+    foreach($feeds as $n => $feed) {
+      if (!empty($feed['XMLURL'])) {
+        $edit = array('title' => $feed['TEXT'], 'url' => $feed['XMLURL']);
+	$blogs[] = $edit;
+      }
+    }
+
+  }
+
+  return $blogs;
+}
+
+
+/**
  * Menu callback; refreshes a feed, then redirects to the overview page.
  *
  * @param $feed
Index: modules/aggregator/aggregator.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.module,v
retrieving revision 1.374
diff -u -p -r1.374 aggregator.module
--- modules/aggregator/aggregator.module	15 Jan 2008 08:06:32 -0000	1.374
+++ modules/aggregator/aggregator.module	8 Feb 2008 11:31:42 -0000
@@ -104,6 +104,15 @@ function aggregator_menu() {
     'parent' => 'admin/content/aggregator',
     'file' => 'aggregator.admin.inc',
   );
+  $items['admin/content/aggregator/add/opml'] = array(
+    'title' => 'Import OPML',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('aggregator_form_opml'),
+    'access arguments' => array('administer news feeds'),
+    'type' => MENU_LOCAL_TASK,
+    'parent' => 'admin/content/aggregator',
+    'file' => 'aggregator.admin.inc',
+  );
   $items['admin/content/aggregator/remove/%aggregator_feed'] = array(
     'title' => 'Remove items',
     'page callback' => 'drupal_get_form',
