diff --git a/feeds.module b/feeds.module index 20ce26a76db973315eab903310b75f92f0b0680b..7875e60f3f293d7e17a969c18522725620232d33 100644 --- a/feeds.module +++ b/feeds.module @@ -928,3 +928,34 @@ function feeds_alter($type, &$data) { /** * @} */ + +/** + * Copy of valid_url() that supports the webcal scheme. + * + * @see valid_url(). + * + * @todo Replace with valid_url() when http://drupal.org/node/1191252 is fixed. + */ +function feeds_valid_url($url, $absolute = FALSE) { + if ($absolute) { + return (bool) preg_match(" + /^ # Start at the beginning of the text + (?:ftp|https?|feed|webcal):\/\/ # Look for ftp, http, https, feed or webcal schemes + (?: # Userinfo (optional) which is typically + (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password + (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination + )? + (?: + (?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address + |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address + ) + (?::[0-9]+)? # Server port number (optional) + (?:[\/|\?] + (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional) + *)? + $/xi", $url); + } + else { + return (bool) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url); + } +} diff --git a/libraries/http_request.inc b/libraries/http_request.inc index 24cc43191788a5f1650a19f9d11df214aea445f2..39b25c6dde65564f5fe676c19118e038d4ef7d16 100644 --- a/libraries/http_request.inc +++ b/libraries/http_request.inc @@ -121,20 +121,36 @@ function http_request_get($url, $username = NULL, $password = NULL, $accept_inva } } + // Support the 'feed' and 'webcal' schemes by converting them into 'http'. + $url = strtr($url, array('feed://' => 'http://', 'webcal://' => 'http://')); + if ($curl) { $headers[] = 'User-Agent: Drupal (+http://drupal.org/)'; $result = new stdClass(); - // Only download via cURL if we can validate the scheme to be either http or - // https. - // Validate in PHP, CURLOPT_PROTOCOLS is only supported with cURL 7.19.4 + // Parse the URL and make sure we can handle the schema. + // cURL can only support either http:// or https://. + // CURLOPT_PROTOCOLS is only supported with cURL 7.19.4 $uri = parse_url($url); - if (isset($uri['scheme']) && $uri['scheme'] != 'http' && $uri['scheme'] != 'https') { - $result->error = 'invalid schema '. $uri['scheme']; - $result->code = -1003; // This corresponds to drupal_http_request() + if (!isset($uri['scheme'])) { + $result->error = 'missing schema'; + $result->code = -1002; } else { + switch ($uri['scheme']) { + case 'http': + case 'https': + // Valid scheme. + break; + default: + $result->error = 'invalid schema ' . $uri['scheme']; + $result->code = -1003; + break; + } + } + // If the scheme was valid, continue to request the feed using cURL. + if (empty($result->error)) { $download = curl_init($url); curl_setopt($download, CURLOPT_FOLLOWLOCATION, TRUE); if (!empty($username)) { diff --git a/plugins/FeedsHTTPFetcher.inc b/plugins/FeedsHTTPFetcher.inc index c8fccfb6528dd6911de55259473611de9e219913..b48d8a1ef4bff4bf343ae3ef248b263619110e78 100644 --- a/plugins/FeedsHTTPFetcher.inc +++ b/plugins/FeedsHTTPFetcher.inc @@ -148,7 +148,11 @@ class FeedsHTTPFetcher extends FeedsFetcher { * Override parent::sourceFormValidate(). */ public function sourceFormValidate(&$values) { - if ($this->config['auto_detect_feeds']) { + if (!feeds_valid_url($values['source'], TRUE)) { + $form_key = 'feeds][' . get_class($this) . '][source'; + form_set_error($form_key, t('The URL %source is invalid.', array('%source' => $values['source']))); + } + elseif ($this->config['auto_detect_feeds']) { feeds_include_library('http_request.inc', 'http_request'); if ($url = http_request_get_common_syndication($values['source'])) { $values['source'] = $url; diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc index f283d9712b7d9e8bdb6b59c3342525a2ed33dc8b..b1925458f1cb997eda1360ff3526df2b032e6686 100644 --- a/plugins/FeedsProcessor.inc +++ b/plugins/FeedsProcessor.inc @@ -163,8 +163,8 @@ abstract class FeedsProcessor extends FeedsPlugin { $messages[] = array( 'message' => format_plural( $state->created, - 'Created @number @entity', - 'Created @number @entities', + 'Created @number @entity.', + 'Created @number @entities.', array('@number' => $state->created) + $tokens ), ); @@ -173,8 +173,8 @@ abstract class FeedsProcessor extends FeedsPlugin { $messages[] = array( 'message' => format_plural( $state->updated, - 'Updated @number @entity', - 'Updated @number @entities', + 'Updated @number @entity.', + 'Updated @number @entities.', array('@number' => $state->updated) + $tokens ), ); @@ -183,8 +183,8 @@ abstract class FeedsProcessor extends FeedsPlugin { $messages[] = array( 'message' => format_plural( $state->failed, - 'Failed importing @number @entity', - 'Failed importing @number @entities', + 'Failed importing @number @entity.', + 'Failed importing @number @entities.', array('@number' => $state->failed) + $tokens ), 'level' => WATCHDOG_ERROR, @@ -443,9 +443,9 @@ abstract class FeedsProcessor extends FeedsPlugin { ); global $user; $formats = filter_formats($user); - foreach ($formats as $format) { - $format_options[$format->format] = check_plain($format->name); - } + foreach ($formats as $format) { + $format_options[$format->format] = check_plain($format->name); + } $form['input_format'] = array( '#type' => 'select', '#title' => t('Text format'), diff --git a/tests/feeds_processor_node.test b/tests/feeds_processor_node.test index 327bc0c9e9fe4db527ffe30c1bddf95cc4392107..476348e8cf00065a1b8b44348a96a2b01f2bae08 100644 --- a/tests/feeds_processor_node.test +++ b/tests/feeds_processor_node.test @@ -31,12 +31,7 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase { // text on nodes will fail. $edit = array('fields[body][type]' => 'text_default'); $this->drupalPost('admin/structure/types/manage/article/display/teaser', $edit, 'Save'); - } - /** - * Test node creation, refreshing/deleting feeds and feed items. - */ - public function test() { // Create an importer configuration. $this->createImporterConfiguration('Syndication', 'syndication'); $this->addMappings('syndication', @@ -68,7 +63,12 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase { ), ) ); + } + /** + * Test node creation, refreshing/deleting feeds and feed items. + */ + public function test() { $nid = $this->createFeedNode(); // Assert 10 items aggregated after creation of the node. @@ -81,9 +81,6 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase { // Navigate to a non-feed node, there should be no Feeds tabs visible. $article_nid = db_query_range("SELECT nid FROM {node} WHERE type = 'article'", 0, 1)->fetchField(); - $this->drupalGet('node/'. $article_nid); - $this->assertNoRaw('node/'. $article_nid .'/import'); - $this->assertNoRaw('node/'. $article_nid .'/delete-items'); $this->assertEqual("Created by FeedsNodeProcessor", db_query("SELECT nr.log FROM {node} n JOIN {node_revision} nr ON n.vid = nr.vid WHERE n.nid = :nid", array(':nid' => $article_nid))->fetchField()); // Assert accuracy of aggregated information. @@ -365,4 +362,48 @@ class FeedsRSStoNodesTest extends FeedsWebTestCase { $this->assertText('Fri, 09/18/2009'); $this->assertText('The first major change is switching'); } + + /** + * Test validation of feed URLs. + */ + function testFeedURLValidation() { + $edit['feeds[FeedsHTTPFetcher][source]'] = 'invalid://url'; + $this->drupalPost('node/add/page', $edit, 'Save'); + $this->assertText('The URL invalid://url is invalid.'); + } + + /** + * Test using non-normal URLs like feed:// and webcal://. + */ + function testOddFeedSchemes() { + $url = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') .'/tests/feeds/developmentseed.rss2'; + + $schemes = array('feed', 'webcal'); + $item_count = 0; + foreach ($schemes as $scheme) { + $feed_url = strtr($url, array('http://' => $scheme . '://', 'https://' => $scheme . '://')); + + $edit['feeds[FeedsHTTPFetcher][source]'] = $feed_url; + $this->drupalPost('node/add/page', $edit, 'Save'); + $this->assertText('Basic page Development Seed - Technological Solutions for Progressive Organizations has been created.'); + $this->assertText('Created 10 nodes.'); + $this->assertFeedItemCount($item_count + 10); + $item_count += 10; + } + } + + /** + * Test that feed elements and links are not found on non-feed nodes. + */ + function testNonFeedNodeUI() { + // There should not be feed links on an article node. + $non_feed_node = $this->drupalCreateNode(array('type' => 'article')); + $this->drupalGet('node/'. $non_feed_node->nid); + $this->assertNoLink('Import'); + $this->assertNoLink('Delete items'); + + // Navigat to a non-feed node form, there should be no Feed field visible. + $this->drupalGet('node/add/article'); + $this->assertNoFieldByName('feeds[FeedsHTTPFetcher][source]'); + } }