Commit 7010dcd1 authored by Dries's avatar Dries

- Patch #16282 by Arancaytar, mustafau, keith.smith, et al: add OPML import...

- Patch #16282 by Arancaytar, mustafau, keith.smith, et al: add OPML import functionality for RSS feeds.  Woot.
parent 9123a3d2
......@@ -19,6 +19,8 @@ Drupal 7.0, xxxx-xx-xx (development version)
slimmed down install profile for developers.
* Image toolkits are added by modules instead of being copied to
the includes directory.
- News aggregator:
* Added OPML import functionality for RSS feeds.
- Search:
* Added support for language-aware searches.
- Testing:
......
......@@ -213,6 +213,151 @@ function aggregator_admin_remove_feed_submit($form, &$form_state) {
$form_state['redirect'] = 'admin/content/aggregator';
}
/**
* Form builder; Generate a form to import feeds from OPML.
*
* @ingroup forms
* @see aggregator_form_opml_validate()
* @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 feeds to be imported.'),
);
$form['remote'] = array(
'#type' => 'textfield',
'#title' => t('OPML Remote URL'),
'#description' => t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'),
);
$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();
$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 both fields are empty or filled, cancel.
if (empty($form_state['values']['remote']) == empty($_FILES['files']['name']['upload'])) {
form_set_error('remote', t('You must <em>either</em> upload a file or enter a URL.'));
}
// Validate the URL, if one was entered.
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) {
$data = '';
if ($file = file_save_upload('upload')) {
$data = file_get_contents($file->filepath);
}
else {
$response = drupal_http_request($form_state['values']['remote']);
if (!isset($response->error)) {
$data = $response->data;
}
}
$feeds = _aggregator_parse_opml($data);
if (empty($feeds)) {
drupal_set_message(t('No new feed has been added.'));
return;
}
$form_state['values']['op'] = t('Save');
foreach ($feeds as $feed) {
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = '%s' OR url = '%s'", $feed['title'], $feed['url']);
$duplicate = FALSE;
while ($old = db_fetch_object($result)) {
if (strcasecmp($old->title, $feed['title']) == 0) {
drupal_set_message(t('A feed named %title already exists.', array('%title' => $old->title)), 'warning');
$duplicate = TRUE;
continue;
}
if (strcasecmp($old->url, $feed['url']) == 0) {
drupal_set_message(t('A feed with the URL %url already exists.', array('%url' => $old->url)), 'warning');
$duplicate = TRUE;
continue;
}
}
if (!$duplicate) {
$form_state['values']['title'] = $feed['title'];
$form_state['values']['url'] = $feed['url'];
drupal_execute('aggregator_form_feed', $form_state);
}
}
}
/**
* Parse an OPML file.
*
* Feeds are recognized as <outline> elements with the attributes
* <em>text</em> and <em>xmlurl</em> set.
*
* @param $opml
* The complete contents of an OPML document.
* @return
* An array of feeds, each an associative array with a <em>title</em> and
* a <em>url</em> element, or NULL if the OPML document failed to be parsed.
* An empty array will be returned if the document is valid but contains
* no feeds, as some OPML documents do.
*/
function _aggregator_parse_opml($opml) {
$feeds = array();
$xml_parser = drupal_xml_parser_create($opml);
if (xml_parse_into_struct($xml_parser, $opml, $values)) {
foreach ($values as $entry) {
if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) {
$item = $entry['attributes'];
if (!empty($item['XMLURL'])) {
$feeds[] = array('title' => $item['TEXT'], 'url' => $item['XMLURL']);
}
}
}
}
xml_parser_free($xml_parser);
return $feeds;
}
/**
* Menu callback; refreshes a feed, then redirects to the overview page.
*
......
......@@ -24,6 +24,8 @@ function aggregator_help($path, $arg) {
return '<p>' . t('Add a feed in RSS, RDF or Atom format. A feed may only have one entry.') . '</p>';
case 'admin/content/aggregator/add/category':
return '<p>' . t('Categories allow feed items from different feeds to be grouped together. For example, several sport-related feeds may belong to a category named <em>Sports</em>. Feed items may be grouped automatically (by selecting a category when creating or editing a feed) or manually (via the <em>Categorize</em> page available from feed item listings). Each category provides its own feed page and block.') . '</p>';
case 'admin/content/aggregator/add/opml':
return '<p>' . t('<acronym title="Outline Processor Markup Language">OPML</acronym> is an XML format used to exchange multiple feeds between aggregators. A single OPML document may contain a collection of many feeds. Drupal can parse such a file and import all feeds at once, saving you the effort of adding them manually. You may either upload a local file from your computer or enter a URL where Drupal can download it.') . '</p>';
}
}
......@@ -101,6 +103,14 @@ function aggregator_menu() {
'type' => MENU_LOCAL_TASK,
'parent' => 'admin/content/aggregator',
);
$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',
);
$items['admin/content/aggregator/remove/%aggregator_feed'] = array(
'title' => 'Remove items',
'page callback' => 'drupal_get_form',
......
......@@ -116,6 +116,85 @@ class AggregatorTestCase extends DrupalWebTestCase {
$result = db_result(db_query("SELECT count(*) FROM {aggregator_feed} WHERE title = '%s' AND url='%s'", $feed_name, $feed_url));
return (1 == $result);
}
/**
* Create a valid OPML file from an array of feeds.
*
* @param $feeds
* An array of feeds.
* @return
* Path to valid OPML file.
*/
function getValidOpml($feeds) {
/**
* Does not have an XML declaration, must pass the parser.
*/
$opml = <<<EOF
<opml version="1.0">
<head></head>
<body>
<!-- First feed to be imported. -->
<outline text="{$feeds[0]['title']}" xmlurl="{$feeds[0]['url']}" />
<!-- Second feed. Test string delimitation and attribute order. -->
<outline xmlurl='{$feeds[1]['url']}' text='{$feeds[1]['title']}'/>
<!-- Test for duplicate URL and title. -->
<outline xmlurl="{$feeds[0]['url']}" text="Duplicate URL"/>
<outline xmlurl="http://duplicate.title" text="{$feeds[1]['title']}"/>
<!-- Test that feeds are only added with required attributes. -->
<outline text="{$feeds[2]['title']}" />
<outline xmlurl="{$feeds[2]['url']}" />
</body>
</opml>
EOF;
$path = file_directory_path() . '/valid-opml.xml';
file_save_data($opml, $path);
return $path;
}
/**
* Create an invalid OPML file.
*
* @return
* Path to invalid OPML file.
*/
function getInvalidOpml() {
$opml = <<<EOF
<opml>
<invalid>
</opml>
EOF;
$path = file_directory_path() . '/invalid-opml.xml';
file_save_data($opml, $path);
return $path;
}
/**
* Create a valid but empty OPML file.
*
* @return
* Path to empty OPML file.
*/
function getEmptyOpml() {
$opml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
<head></head>
<body>
<outline text="Sample text" />
<outline text="Sample text" url="Sample URL" />
</body>
</opml>
EOF;
$path = file_directory_path() . '/empty-opml.xml';
file_save_data($opml, $path);
return $path;
}
}
class AddFeedTestCase extends AggregatorTestCase {
......@@ -312,3 +391,121 @@ class CategorizeFeedItemTestCase extends AggregatorTestCase {
$this->deleteFeed($feed);
}
}
class ImportOPMLTestCase extends AggregatorTestCase {
private static $prefix = 'simpletest_aggregator_';
/**
* Implementation of getInfo().
*/
function getInfo() {
return array(
'name' => t('Import feeds from OPML functionality'),
'description' => t('Test OPML import.'),
'group' => t('Aggregator'),
);
}
/**
* Open OPML import form.
*/
function openImportForm() {
db_query('TRUNCATE {aggregator_category}');
$category = $this->randomName(10, self::$prefix);
db_query("INSERT INTO {aggregator_category} (cid, title, description) VALUES (%d, '%s', '%s')", 1, $category, '');
$this->drupalGet('admin/content/aggregator/add/opml');
$this->assertText('A single OPML document may contain a collection of many feeds.', t('Looking for help text.'));
$this->assertFieldByName('files[upload]', '', t('Looking for file upload field.'));
$this->assertFieldByName('remote', '', t('Looking for remote URL field.'));
$this->assertFieldByName('refresh', '', t('Looking for refresh field.'));
$this->assertFieldByName('category[1]', '1', t('Looking for category field.'));
}
/**
* Submit form filled with invalid fields.
*/
function validateImportFormFields() {
$before = db_result(db_query('SELECT COUNT(*) FROM {aggregator_feed}'));
$form = array();
$this->drupalPost('admin/content/aggregator/add/opml', $form, t('Import'));
$this->assertRaw(t('You must <em>either</em> upload a file or enter a URL.'), t('Error if no fields are filled.'));
$path = $this->getEmptyOpml();
$form = array(
'files[upload]' => $path,
'remote' => file_create_url($path),
);
$this->drupalPost('admin/content/aggregator/add/opml', $form, t('Import'));
$this->assertRaw(t('You must <em>either</em> upload a file or enter a URL.'), t('Error if both fields are filled.'));
$form = array('remote' => 'invalidUrl://empty');
$this->drupalPost('admin/content/aggregator/add/opml', $form, t('Import'));
$this->assertText(t('This URL is not valid.'), t('Error if the URL is invalid.'));
$after = db_result(db_query('SELECT COUNT(*) FROM {aggregator_feed}'));
$this->assertEqual($before, $after, t('No feeds were added during the three last form submissions.'));
}
/**
* Submit form with invalid, empty and valid OPML files.
*/
function submitImportForm() {
$before = db_result(db_query('SELECT COUNT(*) FROM {aggregator_feed}'));
$form['files[upload]'] = $this->getInvalidOpml();
$this->drupalPost('admin/content/aggregator/add/opml', $form, t('Import'));
$this->assertText(t('No new feed has been added.'), t('Attempting to upload invalid XML.'));
$form = array('remote' => file_create_url($this->getEmptyOpml()));
$this->drupalPost('admin/content/aggregator/add/opml', $form, t('Import'));
$this->assertText(t('No new feed has been added.'), t('Attempting to load empty OPML from remote URL.'));
$after = db_result(db_query('SELECT COUNT(*) FROM {aggregator_feed}'));
$this->assertEqual($before, $after, t('No feeds were added during the two last form submissions.'));
db_query('TRUNCATE {aggregator_feed}');
db_query('TRUNCATE {aggregator_category}');
db_query('TRUNCATE {aggregator_category_feed}');
$category = $this->randomName(10, self::$prefix);
db_query("INSERT INTO {aggregator_category} (cid, title, description) VALUES (%d, '%s', '%s')", 1, $category, '');
$feeds[0] = $this->getFeedEditArray();
$feeds[1] = $this->getFeedEditArray();
$feeds[2] = $this->getFeedEditArray();
$form = array(
'files[upload]' => $this->getValidOpml($feeds),
'refresh' => '900',
'category[1]' => $category,
);
$this->drupalPost('admin/content/aggregator/add/opml', $form, t('Import'));
$this->assertRaw(t('A feed with the URL %url already exists.', array('%url' => $feeds[0]['url'])), t('Verifying that a duplicate URL was identified'));
$this->assertRaw(t('A feed named %title already exists.', array('%title' => $feeds[1]['title'])), t('Verifying that a duplicate title was identified'));
$after = db_result(db_query('SELECT COUNT(*) FROM {aggregator_feed}'));
$this->assertEqual($after, 2, t('Verifying that two distinct feeds were added.'));
$feeds_from_db = db_query("SELECT f.title, f.url, f.refresh, cf.cid FROM {aggregator_feed} f LEFT JOIN {aggregator_category_feed} cf ON f.fid = cf.fid");
$refresh = $category = TRUE;
while ($feed = db_fetch_array($feeds_from_db)) {
$title[$feed['url']] = $feed['title'];
$url[$feed['title']] = $feed['url'];
$category = $category && $feed['cid'] == 1;
$refresh = $refresh && $feed['refresh'] == 900;
}
$this->assertEqual($title[$feeds[0]['url']], $feeds[0]['title'], t('First feed was added correctly.'));
$this->assertEqual($url[$feeds[1]['title']], $feeds[1]['url'], t('Second feed was added correctly.'));
$this->assertTrue($refresh, t('Refresh times are correct.'));
$this->assertTrue($category, t('Categories are correct.'));
}
function testOPMLImport() {
$this->openImportForm();
$this->validateImportFormFields();
$this->submitImportForm();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment