diff --git a/rsscloud/notification-request.php b/rsscloud/notification-request.php index 0eecf06..2690c4e 100644 --- a/rsscloud/notification-request.php +++ b/rsscloud/notification-request.php @@ -44,12 +44,6 @@ function rsscloud_hub_process_notification_request( ) { $scheme = ( 443 === $port ) ? 'https://' : 'http://'; - $allow_port = function ( $ports ) use ( $port ) { - $ports[] = $port; - return $ports; - }; - add_filter( 'http_allowed_safe_ports', $allow_port ); - if ( !empty( $_POST['domain'] ) ) { $domain = str_replace( '@', '', sanitize_text_field( wp_unslash( $_POST['domain'] ) ) ); $notify_url = $domain . ':' . $port . $path; @@ -66,8 +60,6 @@ function rsscloud_hub_process_notification_request( ) { $result = wp_safe_remote_post( $notify_url, array( 'method' => 'POST', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, 'body' => array( 'url' => esc_url_raw( wp_unslash( $_POST['url1'] ) ) ) ) ); } - remove_filter( 'http_allowed_safe_ports', $allow_port ); - if ( is_wp_error( $result ) ) rsscloud_notify_result( 'false', 'Error testing notification URL : ' . $result->get_error_message() ); diff --git a/rsscloud/readme.txt b/rsscloud/readme.txt index 9a45ff2..fa5e697 100644 --- a/rsscloud/readme.txt +++ b/rsscloud/readme.txt @@ -1,7 +1,7 @@ === Plugin Name === Contributors: josephscott, automattic Tags: rss -Requires at least: 2.8 +Requires at least: 3.6 Tested up to: 7.0.0 Stable tag: 0.5.1 @@ -15,11 +15,10 @@ Adds RSSCloud ( http://rsscloud.co/ ) capabilities to your RSS feed. = 0.5.1 = * Harden plugin files against direct access and escape values in the cloud element output -* Allow notifications to subscribers using non-standard ports +* Add the source:cloud element so newer feed readers get the https notification URL * Allow notifications to https subscribers if port 443 is specified * More reliable detection of failed notification requests * Reject subscription requests with no domain when the remote address is unavailable -* Use port 443 in cloud tag when site is https = 0.5.0 = * Updates to support PHP 8+ diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index 5cae602..9c62c4c 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -23,6 +23,9 @@ if ( !defined( 'RSSCLOUD_HTTP_TIMEOUT' ) ) define( 'RSSCLOUD_HTTP_TIMEOUT', 3 ); +if ( !defined( 'RSSCLOUD_SOURCE_NS' ) ) + define( 'RSSCLOUD_SOURCE_NS', 'https://source.scripting.com/' ); + require dirname( __FILE__ ) . '/data-storage.php'; if ( !function_exists( 'rsscloud_hub_process_notification_request' ) ) @@ -52,11 +55,11 @@ function rsscloud_parse_request( $wp ) { if ( !function_exists( 'rsscloud_notify_result' ) ) { function rsscloud_notify_result( $success, $msg ) { - $success = strip_tags( $success ); + $success = wp_strip_all_tags( $success ); $success = ent2ncr( $success ); $success = esc_html( $success ); - $msg = strip_tags( $msg ); + $msg = wp_strip_all_tags( $msg ); $msg = ent2ncr( $msg ); $msg = esc_html( $msg ); @@ -73,13 +76,11 @@ function rsscloud_add_rss_cloud_element( ) { return; } - $cloud = parse_url( get_option( 'home' ) . '/?rsscloud=notify' ); + $notify_url = get_option( 'home' ) . '/?rsscloud=notify'; - if ( isset( $cloud['port'] ) ) { - $cloud['port'] = (int) $cloud['port']; - } else { - $cloud['port'] = ( isset( $cloud['scheme'] ) && 'https' === $cloud['scheme'] ) ? 443 : 80; - } + $cloud = parse_url( $notify_url ); + + $cloud['port'] = isset( $cloud['port'] ) ? (int) $cloud['port'] : 80; $cloud['path'] .= "?{$cloud['query']}"; @@ -89,6 +90,17 @@ function rsscloud_add_rss_cloud_element( ) { echo " path='" . esc_attr( $cloud['path'] ) . "' registerProcedure=''"; echo " protocol='http-post' />"; echo "\n"; + + // Newer feed readers use source:cloud, which carries the full notification + // URL and so can advertise https where the cloud element above cannot. + // The namespace is declared on the element rather than via the rss2_ns + // action: declaring it here scopes it to this element, so another plugin + // binding the source prefix on the rss tag can neither collide with this + // declaration nor rebind the prefix out from under it. + echo ""; + echo esc_url( $notify_url ); + echo ''; + echo "\n"; } function rsscloud_generate_challenge( $length = 30 ) { diff --git a/rsscloud/send-post-notifications.php b/rsscloud/send-post-notifications.php index c9ba56a..63638ba 100644 --- a/rsscloud/send-post-notifications.php +++ b/rsscloud/send-post-notifications.php @@ -30,16 +30,8 @@ function rsscloud_send_post_notifications( $rss2_url = false ) { if ( !empty( $url['port'] ) ) $port = $url['port']; - $allow_port = function ( $ports ) use ( $port ) { - $ports[] = (int) $port; - return $ports; - }; - add_filter( 'http_allowed_safe_ports', $allow_port ); - $result = wp_safe_remote_post( $notify_url, array( 'method' => 'POST', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, 'body' => array( 'url' => $rss2_url ) ) ); - remove_filter( 'http_allowed_safe_ports', $allow_port ); - do_action( 'rsscloud_send_notification' ); if ( !is_wp_error( $result ) ) diff --git a/tests/test-notification-request.php b/tests/test-notification-request.php index 78305de..626ad3b 100644 --- a/tests/test-notification-request.php +++ b/tests/test-notification-request.php @@ -535,42 +535,6 @@ function ( $preempt, $args, $url ) { $this->assertArrayHasKey( 'http://callback.example.com:9000/notify', $notify[ $this->feed_url ] ); } - public function test_nonstandard_port_is_whitelisted_for_url_validation() { - // No pre_http_request mock — wp_http_validate_url only runs when the - // request isn't short-circuited, and that's where - // http_allowed_safe_ports gets applied. Hook it at priority 999 so - // we observe what the plugin handed to the validator. - $observed_ports = array(); - add_filter( - 'http_allowed_safe_ports', - function ( $ports ) use ( &$observed_ports ) { - $observed_ports = $ports; - return $ports; - }, - 999 - ); - - // Use a TEST-NET-3 IP (RFC 5737) so wp_http_validate_url skips DNS - // and reaches the port check where our filter runs. - $_POST = array( - 'url1' => $this->feed_url, - 'port' => '4000', - 'path' => '/feedupdated', - 'domain' => '203.0.113.5', - ); - - // Request will fail at the network layer (connection refused); we - // don't care about the outcome, only that validation was reached - // with our port whitelisted. - $this->call_process_notification_request(); - - $this->assertContains( - 4000, - $observed_ports, - 'Plugin should add the subscriber port to http_allowed_safe_ports before sending.' - ); - } - public function test_port_443_uses_https_scheme_for_domain_based() { add_filter( 'pre_http_request', diff --git a/tests/test-rsscloud.php b/tests/test-rsscloud.php index 939af50..1b0d52b 100644 --- a/tests/test-rsscloud.php +++ b/tests/test-rsscloud.php @@ -76,14 +76,14 @@ public function test_add_rss_cloud_element_uses_port_from_home_url() { $this->assertStringContainsString( "port='" . $port . "'", $output ); } - public function test_add_rss_cloud_element_defaults_port_to_443_for_https_home() { + public function test_add_rss_cloud_element_escapes_attribute_values() { $this->go_to( get_feed_link( 'rss2' ) ); // Override home AFTER go_to so is_feed() still works. add_filter( 'option_home', function () { - return 'https://example.com'; + return 'http://example.com:8080/path?a=1&b=2'; } ); @@ -91,17 +91,50 @@ function () { rsscloud_add_rss_cloud_element(); $output = ob_get_clean(); - $this->assertStringContainsString( "port='443'", $output ); + // With proper escaping, '&' in attribute values should become '&'. + $this->assertStringNotContainsString( '&b=2', $output, + 'Attribute values must be escaped; raw & should not appear' ); + $this->assertStringContainsString( '&', $output, + 'Attribute values should use esc_attr() which converts & to &' ); } - public function test_add_rss_cloud_element_escapes_attribute_values() { + public function test_add_rss_cloud_element_outputs_source_cloud_tag() { + $this->go_to( get_feed_link( 'rss2' ) ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'assertStringContainsString( '', $output ); + $this->assertStringContainsString( 'rsscloud=notify', $output ); + } + + public function test_source_cloud_declares_namespace_on_the_element() { + $this->go_to( get_feed_link( 'rss2' ) ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // The namespace must be declared on the element itself, not left to + // the rss2_ns action, so another plugin binding the source prefix on + // the rss tag cannot collide with it or rebind it. + $this->assertStringContainsString( + "xmlns:source='https://source.scripting.com/'", + $output, + 'source:cloud must carry its own namespace declaration' + ); + } + + public function test_source_cloud_uses_full_url_with_site_scheme() { $this->go_to( get_feed_link( 'rss2' ) ); // Override home AFTER go_to so is_feed() still works. add_filter( 'option_home', function () { - return 'http://example.com:8080/path?a=1&b=2'; + return 'https://secure.example.com'; } ); @@ -109,11 +142,69 @@ function () { rsscloud_add_rss_cloud_element(); $output = ob_get_clean(); - // With proper escaping, '&' in attribute values should become '&'. + // The point of source:cloud: it carries the https endpoint that the + // port-80 / http-post cloud element above cannot express. + $this->assertStringContainsString( + '>https://secure.example.com/?rsscloud=notify', + $output + ); + + // The classic cloud element must stay on port 80 / http-post for + // older readers such as FeedLand. + $this->assertStringContainsString( "port='80'", $output ); + $this->assertStringContainsString( "protocol='http-post'", $output ); + } + + public function test_source_cloud_url_is_escaped() { + $this->go_to( get_feed_link( 'rss2' ) ); + + add_filter( + 'option_home', + function () { + return 'https://example.com/path?a=1&b=2'; + } + ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // Raw & is not well-formed XML in element content. $this->assertStringNotContainsString( '&b=2', $output, - 'Attribute values must be escaped; raw & should not appear' ); - $this->assertStringContainsString( '&', $output, - 'Attribute values should use esc_attr() which converts & to &' ); + 'source:cloud URL must be escaped; raw & should not appear' ); + } + + public function test_feed_with_source_cloud_is_well_formed_xml() { + $this->go_to( get_feed_link( 'rss2' ) ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // Wrap the fragment the way feed-rss2.php does, including another + // plugin binding the source prefix to a DIFFERENT uri on the rss tag, + // and confirm the result still parses and resolves correctly. + $xml = '' + . '' + . 't' . $output . ''; + + $prev = libxml_use_internal_errors( true ); + libxml_clear_errors(); + $doc = simplexml_load_string( $xml ); + $errors = libxml_get_errors(); + libxml_clear_errors(); + libxml_use_internal_errors( $prev ); + + $this->assertNotFalse( $doc, 'Feed fragment must be well-formed XML' ); + $this->assertEmpty( $errors, 'Feed fragment must parse without XML errors' ); + + // The inline declaration must win over the rss tag's binding. + $source = $doc->channel->children( 'https://source.scripting.com/' ); + $this->assertNotEmpty( + (string) $source->cloud, + 'source:cloud must resolve to the scripting.com namespace, not the rss tag binding' + ); + $this->assertStringContainsString( 'rsscloud=notify', (string) $source->cloud ); } public function test_parse_request_does_nothing_without_rsscloud_var() { diff --git a/tests/test-send-post-notifications.php b/tests/test-send-post-notifications.php index c903628..62115b0 100644 --- a/tests/test-send-post-notifications.php +++ b/tests/test-send-post-notifications.php @@ -274,37 +274,4 @@ function () use ( &$fired ) { $this->assertTrue( $fired ); } - - public function test_nonstandard_port_is_whitelisted_for_url_validation() { - // No pre_http_request mock — we want wp_http_validate_url to run so - // the plugin's http_allowed_safe_ports filter gets applied. Hook at - // priority 999 to observe what the validator sees. - $observed_ports = array(); - add_filter( - 'http_allowed_safe_ports', - function ( $ports ) use ( &$observed_ports ) { - $observed_ports = $ports; - return $ports; - }, - 999 - ); - - // TEST-NET-3 IP (RFC 5737) — wp_http_validate_url skips DNS for - // literal IPs and reaches the port check where our filter fires. - $this->set_notifications( - $this->build_notifications( - array( - 'notify_url' => 'http://203.0.113.5:4000/feedupdated', - ) - ) - ); - - rsscloud_send_post_notifications( $this->feed_url ); - - $this->assertContains( - 4000, - $observed_ports, - 'Plugin should add the subscriber port to http_allowed_safe_ports before sending.' - ); - } }