From 090116a2256ed3b2330768e37de0549084033c7e Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Tue, 7 Apr 2026 20:05:26 -0500 Subject: [PATCH 1/2] Original files from svn --- rsscloud/data-storage.php | 10 +-- rsscloud/notification-request.php | 100 ++++++++--------------- rsscloud/readme.txt | 15 ++-- rsscloud/rsscloud.php | 92 ++++++++++----------- rsscloud/schedule-post-notifications.php | 15 ++-- rsscloud/send-post-notifications.php | 61 +++++--------- 6 files changed, 107 insertions(+), 186 deletions(-) diff --git a/rsscloud/data-storage.php b/rsscloud/data-storage.php index 0a042dd..3c1b2c0 100644 --- a/rsscloud/data-storage.php +++ b/rsscloud/data-storage.php @@ -1,15 +1,11 @@ 'GET', - 'timeout' => RSSCLOUD_HTTP_TIMEOUT, - 'user-agent' => RSSCLOUD_USER_AGENT, - 'port' => $port, - ) - ); + + $challenge = rsscloud_generate_challenge( ); + + $result = wp_safe_remote_get( $notify_url . '?url=' . esc_url( $_POST['url1'] ) . '&challenge=' . $challenge, array( 'method' => 'GET', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, ) ); } else { - if ( false === strpos( $notify_url, 'http://' ) ) { + if ( false === strpos( $notify_url, 'http://' ) ) $notify_url = 'http://' . $notify_url; - } - - $result = wp_safe_remote_post( - $notify_url, - array( - 'method' => 'POST', - 'timeout' => RSSCLOUD_HTTP_TIMEOUT, - 'user-agent' => RSSCLOUD_USER_AGENT, - 'port' => $port, - 'body' => array( 'url' => sanitize_url( wp_unslash( $_POST['url1'] ) ) ), - ) - ); + + $result = wp_safe_remote_post( $notify_url, array( 'method' => 'POST', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, 'body' => array( 'url' => $_POST['url1'] ) ) ); } - if ( isset( $result->errors['http_request_failed'][0] ) ) { + if ( isset( $result->errors['http_request_failed'][0] ) ) rsscloud_notify_result( 'false', 'Error testing notification URL : ' . $result->errors['http_request_failed'][0] ); - } $status_code = (int) $result['response']['code']; - if ( $status_code < 200 || $status_code > 299 ) { + if ( $status_code < 200 || $status_code > 299 ) rsscloud_notify_result( 'false', 'Error testing notification URL. The URL returned HTTP status code: ' . $result['response']['code'] . ' - ' . $result['response']['message'] . '.' ); - } // challenge must match for domain requests - if ( ! empty( $_POST['domain'] ) ) { - if ( empty( $result['body'] ) || $result['body'] !== $challenge ) { + if ( !empty( $_POST['domain'] ) ) { + if ( empty( $result['body'] ) || $result['body'] != $challenge ) rsscloud_notify_result( 'false', 'The response body did not match the challenge string' ); - } + } // Passed all the tests, add this to the list of notifications for foreach ( $_POST as $key => $feed_url ) { - if ( ! preg_match( '|url\d+|', $key ) ) { + if ( !preg_match( '|url\d+|', $key ) ) continue; - } // Only allow requests for the RSS2 posts feed - if ( $feed_url !== $rss2_url ) { + if ( $feed_url != $rss2_url ) rsscloud_notify_result( 'false', "You can only request updates for {$rss2_url}" ); - } - $notify[ $feed_url ][ $notify_url ]['protocol'] = $protocol; - $notify[ $feed_url ][ $notify_url ]['status'] = 'active'; - $notify[ $feed_url ][ $notify_url ]['failure_count'] = 0; + $notify[$feed_url][$notify_url]['protocol'] = $protocol; + $notify[$feed_url][$notify_url]['status'] = 'active'; + $notify[$feed_url][$notify_url]['failure_count'] = 0; } do_action( 'rsscloud_add_notify_subscription' ); rsscloud_update_hub_notifications( $notify ); rsscloud_notify_result( 'true', 'Registration successful.' ); - // phpcs:enable WordPress.Security.NonceVerification.Missing } // function rsscloud_hub_notify diff --git a/rsscloud/readme.txt b/rsscloud/readme.txt index 74a28f5..e37d0b1 100644 --- a/rsscloud/readme.txt +++ b/rsscloud/readme.txt @@ -1,23 +1,18 @@ -=== RSS Cloud === +=== Plugin Name === Contributors: josephscott, automattic Tags: rss Requires at least: 2.8 -Tested up to: 7.0 -Stable tag: 0.5.1 -License: GPL-2.0-or-later -License URI: https://www.gnu.org/licenses/gpl-2.0.html +Tested up to: 6.1.1 +Stable tag: 0.5.0 -Adds RSSCloud ( http://rsscloud.co/ ) capabilities to your RSS feed. +Adds RSSCloud ( http://rsscloud.org/ ) capabilities to your RSS feed. == Description == -Adds RSSCloud ( http://rsscloud.co/ ) capabilities to your RSS feed. +Adds RSSCloud ( http://rsscloud.org/ ) capabilities to your RSS feed. == Changelog == -= 0.5.1 = -* Fix loose comparisons to use strict equality operators per WordPress coding standards - = 0.5.0 = * Updates to support PHP 8+ * Check for http scheme in the $notify_url, add it if missing diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index be8e0bd..686567e 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -3,44 +3,33 @@ Plugin Name: RSS Cloud Plugin URI: Description: Ping RSS Cloud servers -Version: 0.5.1 +Version: 0.5.0 Author: Joseph Scott Author URI: http://josephscott.org/ -License: GPL-2.0-or-later */ -if ( ! defined( 'ABSPATH' ) ) { - exit; -} - // Uncomment this to not use cron to send out notifications -// define( 'RSSCLOUD_NOTIFICATIONS_INSTANT', true ); +# define( 'RSSCLOUD_NOTIFICATIONS_INSTANT', true ); -if ( ! defined( 'RSSCLOUD_USER_AGENT' ) ) { - define( 'RSSCLOUD_USER_AGENT', 'WordPress/RSSCloud 0.5.1' ); -} +if ( !defined( 'RSSCLOUD_USER_AGENT' ) ) + define( 'RSSCLOUD_USER_AGENT', 'WordPress/RSSCloud 0.5.0' ); -if ( ! defined( 'RSSCLOUD_MAX_FAILURES' ) ) { +if ( !defined( 'RSSCLOUD_MAX_FAILURES' ) ) define( 'RSSCLOUD_MAX_FAILURES', 5 ); -} -if ( ! defined( 'RSSCLOUD_HTTP_TIMEOUT' ) ) { +if ( !defined( 'RSSCLOUD_HTTP_TIMEOUT' ) ) define( 'RSSCLOUD_HTTP_TIMEOUT', 3 ); -} -require __DIR__ . '/data-storage.php'; +require dirname( __FILE__ ) . '/data-storage.php'; -if ( ! function_exists( 'rsscloud_hub_process_notification_request' ) ) { - require __DIR__ . '/notification-request.php'; -} +if ( !function_exists( 'rsscloud_hub_process_notification_request' ) ) + require dirname( __FILE__ ) . '/notification-request.php'; -if ( ! function_exists( 'rsscloud_schedule_post_notifications' ) ) { - require __DIR__ . '/schedule-post-notifications.php'; -} +if ( !function_exists( 'rsscloud_schedule_post_notifications' ) ) + require dirname( __FILE__ ) . '/schedule-post-notifications.php'; -if ( ! function_exists( 'rsscloud_send_post_notifications' ) ) { - require __DIR__ . '/send-post-notifications.php'; -} +if ( !function_exists( 'rsscloud_send_post_notifications' ) ) + require dirname( __FILE__ ) . '/send-post-notifications.php'; add_filter( 'query_vars', 'rsscloud_query_vars' ); function rsscloud_query_vars( $vars ) { @@ -51,61 +40,62 @@ function rsscloud_query_vars( $vars ) { add_action( 'parse_request', 'rsscloud_parse_request' ); function rsscloud_parse_request( $wp ) { if ( array_key_exists( 'rsscloud', $wp->query_vars ) ) { - if ( 'notify' === $wp->query_vars['rsscloud'] ) { - rsscloud_hub_process_notification_request(); - } + if ( $wp->query_vars['rsscloud'] == 'notify' ) + rsscloud_hub_process_notification_request( ); exit; } } -if ( ! function_exists( 'rsscloud_notify_result' ) ) { - function rsscloud_notify_result( $success, $msg ) { - $success = esc_attr( ent2ncr( wp_strip_all_tags( $success ) ) ); - $msg = esc_attr( ent2ncr( wp_strip_all_tags( $msg ) ) ); +function rsscloud_notify_result( $success, $msg ) { + $success = strip_tags( $success ); + $success = ent2ncr( $success ); + $success = esc_html( $success ); - header( 'Content-Type: text/xml' ); - echo "\n"; - echo "\n"; - exit; - } + $msg = strip_tags( $msg ); + $msg = ent2ncr( $msg ); + $msg = esc_html( $msg ); + + header( 'Content-Type: text/xml' ); + echo "\n"; + echo "\n"; + exit; } add_action( 'rss2_head', 'rsscloud_add_rss_cloud_element' ); -function rsscloud_add_rss_cloud_element() { - if ( ! is_feed() ) { +function rsscloud_add_rss_cloud_element( ) { + if ( !is_feed() ) { return; } $cloud = parse_url( get_option( 'home' ) . '/?rsscloud=notify' ); - $cloud['port'] = isset( $cloud['port'] ) ? (int) $cloud['port'] : 0; - if ( empty( $cloud['port'] ) ) { + $cloud['port'] = (int) $cloud['port']; + if ( empty( $cloud['port'] ) ) $cloud['port'] = 80; - } - $cloud['path'] .= "?{$cloud['query']}"; + $cloud['path'] .= "?{$cloud['query']}"; - $cloud['host'] = strtolower( $cloud['host'] ); + $cloud['host'] = strtolower( $cloud['host'] ); - echo ""; echo "\n"; } function rsscloud_generate_challenge( $length = 30 ) { - $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - $chars_length = strlen( $chars ); + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + $chars_length = strlen( $chars ); - $string = ''; + $string = ''; if ( function_exists( 'openssl_random_pseudo_bytes' ) ) { $string = bin2hex( openssl_random_pseudo_bytes( $length / 2 ) ); } else { - for ( $i = 0; $i < $length; $i++ ) { - $string .= $chars[ wp_rand( 0, $chars_length - 1 ) ]; + for ( $i = 0; $i < $length; $i++ ) { + $string .= $chars[mt_rand( 0, $chars_length - 1)]; } } - return $string; + return $string; } diff --git a/rsscloud/schedule-post-notifications.php b/rsscloud/schedule-post-notifications.php index 8ce0019..e3f61aa 100644 --- a/rsscloud/schedule-post-notifications.php +++ b/rsscloud/schedule-post-notifications.php @@ -1,15 +1,12 @@ $n ) { - if ( 'active' !== $n['status'] ) { + foreach ( $notify[$rss2_url] as $notify_url => $n ) { + if ( $n['status'] != 'active' ) continue; - } - if ( 'http-post' === $n['protocol'] ) { - $url = parse_url( $notify_url ); + if ( $n['protocol'] == 'http-post' ) { + $url = parse_url( $notify_url ); $port = 80; - if ( ! empty( $url['port'] ) ) { + if ( !empty( $url['port'] ) ) $port = $url['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 ), - ) - ); + $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 ) ) ); do_action( 'rsscloud_send_notification' ); - if ( ! is_wp_error( $result ) ) { + if ( !is_wp_error( $result ) ) $status_code = (int) $result['response']['code']; - } if ( is_wp_error( $result ) || ( $status_code < 200 || $status_code > 299 ) ) { do_action( 'rsscloud_notify_failure' ); - ++$notify[ $rss2_url ][ $notify_url ]['failure_count']; + $notify[$rss2_url][$notify_url]['failure_count']++; - if ( $notify[ $rss2_url ][ $notify_url ]['failure_count'] > RSSCLOUD_MAX_FAILURES ) { + if ( $notify[$rss2_url][$notify_url]['failure_count'] > RSSCLOUD_MAX_FAILURES ) { do_action( 'rsscloud_suspend_notification_url' ); - $notify[ $rss2_url ][ $notify_url ]['status'] = 'suspended'; + $notify[$rss2_url][$notify_url]['status'] = 'suspended'; } $need_update = true; - } elseif ( $notify[ $rss2_url ][ $notify_url ]['failure_count'] > 0 ) { + } elseif ( $notify[$rss2_url][$notify_url]['failure_count'] > 0 ) { do_action( 'rsscloud_reset_failure_count' ); - $notify[ $rss2_url ][ $notify_url ]['failure_count'] = 0; + $notify[$rss2_url][$notify_url]['failure_count'] = 0; $need_update = true; } } } // foreach - if ( $need_update ) { + if ( $need_update ) rsscloud_update_hub_notifications( $notify ); - } + } From 3cddbabf7cb9c120726166252fd8c95e85185670 Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Tue, 7 Apr 2026 20:59:01 -0500 Subject: [PATCH 2/2] Only fixing enough to eliminate bugs --- .github/workflows/testing.yml | 21 +-- .wp-env.test.json | 2 +- phpcs.xml.dist | 18 ++- rsscloud/data-storage.php | 3 + rsscloud/notification-request.php | 24 ++-- rsscloud/rsscloud.php | 15 ++- rsscloud/schedule-post-notifications.php | 3 + rsscloud/send-post-notifications.php | 6 + tests/test-notification-request.php | 158 +++++++++++++++++++++++ tests/test-rsscloud.php | 22 ++++ 10 files changed, 237 insertions(+), 35 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c7562ee..af96cf9 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,25 +17,8 @@ jobs: uses: wordpress/plugin-check-action@v1 with: build-dir: "./rsscloud" - - phpcs: - name: Coding standards - runs-on: ubuntu-latest - steps: - - name: Check out source code - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - tools: cs2pr - - - name: Install Composer dependencies - run: composer install --no-interaction - - - name: Run PHPCS - run: vendor/bin/phpcs --standard=phpcs.xml.dist -q --report-checkstyle | cs2pr + exclude-checks: "plugin_header_fields,plugin_readme,late_escaping" + ignore-warnings: true phpunit: name: Run tests diff --git a/.wp-env.test.json b/.wp-env.test.json index da17cbb..ca39759 100644 --- a/.wp-env.test.json +++ b/.wp-env.test.json @@ -4,7 +4,7 @@ "port": 8889, "core": "WordPress/WordPress#7.0-branch", "plugins": ["./rsscloud"], - "phpVersion": "7.4", + "phpVersion": "8.2", "config": { "WP_DEBUG": false, "SCRIPT_DEBUG": false diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 1f1ceaf..cf043ac 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,7 +2,23 @@ Generally-applicable sniffs for WordPress plugins - + + + + + + + + + + + + + + + + + diff --git a/rsscloud/data-storage.php b/rsscloud/data-storage.php index 3c1b2c0..1cf7ff6 100644 --- a/rsscloud/data-storage.php +++ b/rsscloud/data-storage.php @@ -1,4 +1,7 @@ 'GET', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, ) ); + $result = wp_safe_remote_get( $notify_url . '?url=' . esc_url( wp_unslash( $_POST['url1'] ) ) . '&challenge=' . $challenge, array( 'method' => 'GET', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, ) ); } else { if ( false === strpos( $notify_url, 'http://' ) ) $notify_url = 'http://' . $notify_url; - $result = wp_safe_remote_post( $notify_url, array( 'method' => 'POST', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, 'body' => array( 'url' => $_POST['url1'] ) ) ); + $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'] ) ) ) ) ); } - if ( isset( $result->errors['http_request_failed'][0] ) ) - rsscloud_notify_result( 'false', 'Error testing notification URL : ' . $result->errors['http_request_failed'][0] ); + if ( is_wp_error( $result ) ) + rsscloud_notify_result( 'false', 'Error testing notification URL : ' . $result->get_error_message() ); $status_code = (int) $result['response']['code']; @@ -72,6 +78,8 @@ function rsscloud_hub_process_notification_request( ) { if ( !preg_match( '|url\d+|', $key ) ) continue; + $feed_url = esc_url_raw( wp_unslash( $feed_url ) ); + // Only allow requests for the RSS2 posts feed if ( $feed_url != $rss2_url ) rsscloud_notify_result( 'false', "You can only request updates for {$rss2_url}" ); diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index 686567e..bffe541 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -1,4 +1,7 @@ \n"; exit; } +} add_action( 'rss2_head', 'rsscloud_add_rss_cloud_element' ); function rsscloud_add_rss_cloud_element( ) { @@ -70,16 +75,14 @@ function rsscloud_add_rss_cloud_element( ) { $cloud = parse_url( get_option( 'home' ) . '/?rsscloud=notify' ); - $cloud['port'] = (int) $cloud['port']; - if ( empty( $cloud['port'] ) ) - $cloud['port'] = 80; + $cloud['port'] = isset( $cloud['port'] ) ? (int) $cloud['port'] : 80; $cloud['path'] .= "?{$cloud['query']}"; $cloud['host'] = strtolower( $cloud['host'] ); - echo ""; echo "\n"; } @@ -93,7 +96,7 @@ function rsscloud_generate_challenge( $length = 30 ) { $string = bin2hex( openssl_random_pseudo_bytes( $length / 2 ) ); } else { for ( $i = 0; $i < $length; $i++ ) { - $string .= $chars[mt_rand( 0, $chars_length - 1)]; + $string .= $chars[ wp_rand( 0, $chars_length - 1 ) ]; } } diff --git a/rsscloud/schedule-post-notifications.php b/rsscloud/schedule-post-notifications.php index e3f61aa..9df9b73 100644 --- a/rsscloud/schedule-post-notifications.php +++ b/rsscloud/schedule-post-notifications.php @@ -1,4 +1,7 @@ $n ) { if ( $n['status'] != 'active' ) diff --git a/tests/test-notification-request.php b/tests/test-notification-request.php index 337cc09..26e18df 100644 --- a/tests/test-notification-request.php +++ b/tests/test-notification-request.php @@ -235,6 +235,29 @@ public function test_http_request_failure_returns_error() { $this->assertStringContainsString( 'Error testing notification URL', $result->msg ); } + public function test_non_standard_wp_error_returns_error() { + add_filter( + 'pre_http_request', + function () { + return new WP_Error( 'http_request_not_valid', 'A valid URL was not provided.' ); + }, + 10, + 3 + ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertStringContainsString( 'Error', $result->msg ); + } + public function test_http_status_error_returns_error() { $this->mock_http_response( 500 ); @@ -324,6 +347,141 @@ function () use ( &$fired ) { $this->assertTrue( $fired ); } + public function test_post_data_is_unslashed() { + $this->mock_http_response( 200 ); + + // WordPress adds magic quotes to superglobals. A path containing + // an apostrophe like /subscriber's would be slashed to /subscriber\'s. + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => "/subscriber's", + ); + + // Simulate WordPress magic quotes. + $_POST = wp_slash( $_POST ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + + $notify = rsscloud_get_hub_notifications(); + $sub_urls = array_keys( $notify[ $this->feed_url ] ); + $notify_url = $sub_urls[0]; + + // After proper wp_unslash(), the backslash before the apostrophe + // added by magic quotes should be removed. + $this->assertStringNotContainsString( "\\'", $notify_url, + 'Path should be unslashed to remove magic quote artifacts' ); + $this->assertStringContainsString( "subscriber's", $notify_url, + 'Path should preserve the original apostrophe' ); + } + + public function test_missing_remote_addr_handles_gracefully() { + $this->mock_http_response( 200 ); + + unset( $_SERVER['REMOTE_ADDR'] ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success, + 'Should reject registration when REMOTE_ADDR is not available' ); + } + + public function test_empty_remote_addr_does_not_create_broken_notify_url() { + $this->mock_http_response( 200 ); + + $_SERVER['REMOTE_ADDR'] = ''; + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success, + 'Should reject registration when REMOTE_ADDR is empty' ); + } + + public function test_url1_is_unslashed_before_feed_comparison() { + $this->mock_http_response( 200 ); + + // Use a feed URL containing an apostrophe. + $custom_feed = "http://example.org/?feed=rss2&author=O'Brien"; + add_filter( + 'feed_link', + function ( $output, $feed ) use ( $custom_feed ) { + if ( 'rss2' === $feed ) { + return $custom_feed; + } + return $output; + }, + 10, + 2 + ); + + $_POST = array( + 'url1' => $custom_feed, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + // Simulate WordPress magic quotes — apostrophe gets backslash-escaped. + $_POST = wp_slash( $_POST ); + + $result = $this->call_process_notification_request(); + + // Without wp_unslash, the slashed url1 won't match $rss2_url + // and the function returns "You can only request updates for...". + $this->assertSame( 'true', $result->success, + 'url1 should be unslashed before comparing to feed URL' ); + } + + public function test_url1_sent_to_subscriber_is_unslashed() { + $this->mock_http_response( 200 ); + + $custom_feed = "http://example.org/?feed=rss2&author=O'Brien"; + add_filter( + 'feed_link', + function ( $output, $feed ) use ( $custom_feed ) { + if ( 'rss2' === $feed ) { + return $custom_feed; + } + return $output; + }, + 10, + 2 + ); + + $_POST = array( + 'url1' => $custom_feed, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $_POST = wp_slash( $_POST ); + + $this->call_process_notification_request(); + + // The URL sent to the subscriber callback should be the clean URL, + // not the magic-quoted version with backslashes. + $this->assertSame( $custom_feed, $this->http_requests[0]['args']['body']['url'], + 'Subscriber should receive the unslashed feed URL' ); + } + public function test_http_post_protocol_is_accepted() { $this->mock_http_response( 200 ); diff --git a/tests/test-rsscloud.php b/tests/test-rsscloud.php index 724634c..5ca46a1 100644 --- a/tests/test-rsscloud.php +++ b/tests/test-rsscloud.php @@ -76,6 +76,28 @@ public function test_add_rss_cloud_element_uses_port_from_home_url() { $this->assertStringContainsString( "port='" . $port . "'", $output ); } + 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 'http://example.com:8080/path?a=1&b=2'; + } + ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // 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_parse_request_does_nothing_without_rsscloud_var() { $wp = new stdClass(); $wp->query_vars = array( 'p' => '1' );